From f10dadbd9f85a018d8e510dca1b882a7f32b7d89 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Tue, 24 Jan 2023 16:53:55 -0800 Subject: [PATCH 01/71] Fixed Toast widths --- apps/webapp/app/root.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 10350dd6983..1f51c81cd6f 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -143,7 +143,7 @@ function App() { background: "#D1FAE5", padding: "16px 20px", color: "#1E293B", - minWidth: "500px", + maxWidth: "500px", }, iconTheme: { primary: "#10B981", @@ -157,7 +157,7 @@ function App() { background: "#FFE4E6", padding: "16px 20px", color: "#1E293B", - minWidth: "500px", + maxWidth: "500px", }, iconTheme: { primary: "#F43F5E", From 88306d6a63a707f09372f94813d992ca61bcb0d2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 25 Jan 2023 15:33:20 -0800 Subject: [PATCH 02/71] Removed private beta message at the start of the README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index c525a9630db..c8951701d9b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -### The Trigger.dev Private beta is now open. Sign up for access [here](https://bcymafitv0e.typeform.com/tddsignup#source=GitHubReadme). - -  - ![Hero](https://raw.githubusercontent.com/triggerdotdev/trigger.dev/eebe37109e33beae6390ee19029fce8a5934c84b/apps/webapp/public/images/logo-banner.png) From d6512b2ba51ab820b2b62e7df6eaf16acc862a76 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 22:13:19 -0800 Subject: [PATCH 03/71] Revert "Merge pull request #28 from triggerdotdev/dev" This reverts commit 92391c48cb41557d4a65482566104fd68aba5413, reversing changes made to c954b3743fbb98a91c84207ae0d7b5c314f7ad17. --- apps/webapp/app/root.tsx | 26 -------------------------- apps/webapp/package.json | 8 ++++---- pnpm-lock.yaml | 2 +- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 83b085f73e4..1f51c81cd6f 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -22,7 +22,6 @@ import { withSentry } from "@sentry/remix"; import { env } from "./env.server"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { PrimaryLink } from "./components/primitives/Buttons"; -import { Body } from "./components/primitives/text/Body"; export const links: LinksFunction = () => { return [ @@ -79,31 +78,6 @@ export function CatchBoundary() { ); } -export function ErrorBoundary({ error }: { error: any }) { - console.error(error); - return ( - - - Oops! - - - - -
-
-

Oh no!

- {JSON.stringify(error)} - - Back home - -
- -
- - - ); -} - function App() { const { toastMessage, posthogProjectKey, user } = useTypedLoaderData(); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 320ae8bad35..3324602ee55 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -63,9 +63,10 @@ "@tailwindcss/forms": "^0.5.2", "@tanstack/react-table": "^8.0.0-alpha.87", "@trigger.dev/common-schemas": "workspace:*", - "@trigger.dev/integrations": "workspace:*", "@trigger.dev/providers": "workspace:*", "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/integrations": "workspace:*", + "internal-bridge": "workspace:*", "@uiw/react-codemirror": "^4.13.2", "bcryptjs": "^2.4.3", "classnames": "^2.3.1", @@ -80,7 +81,6 @@ "emails": "workspace:*", "express": "^4.18.1", "humanize-duration": "^3.27.3", - "internal-bridge": "workspace:*", "internal-integrations": "workspace:*", "internal-platform": "workspace:*", "internal-pulsar": "workspace:*", @@ -112,7 +112,7 @@ "remix-auth": "^3.2.2", "remix-auth-email-link": "^1.4.2", "remix-auth-github": "^1.1.1", - "remix-typedjson": "~0.1.3", + "remix-typedjson": "^0.1.3", "remix-utils": "^3.4.0", "slug": "^6.0.0", "tiny-invariant": "^1.2.0", @@ -186,4 +186,4 @@ "engines": { "node": ">=16.0.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72d809741d2..36d67f12f19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,7 +164,7 @@ importers: remix-auth: ^3.2.2 remix-auth-email-link: ^1.4.2 remix-auth-github: ^1.1.1 - remix-typedjson: ~0.1.3 + remix-typedjson: ^0.1.3 remix-utils: ^3.4.0 slug: ^6.0.0 start-server-and-test: ^1.14.0 From 888c8a7460b08797c1649fa62c8c59d0ff6f063d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 22:13:35 -0800 Subject: [PATCH 04/71] Revert "Merge pull request #27 from triggerdotdev/dev" This reverts commit c954b3743fbb98a91c84207ae0d7b5c314f7ad17, reversing changes made to 26c1c9f4b228933ef022e80e5fa0ae179580de08. --- .../app/components/CreateNewWorkflow.tsx | 18 +++++++++++++----- apps/webapp/app/hooks/useOrganizations.ts | 16 ++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 2e2d061888a..591f8337cea 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -84,14 +84,22 @@ export function CreateNewWorkflowNoWorkflows() { Easily authenticate with APIs using the supported integrations below. If there's an integration we don't yet support,{" "} - + {" "} and we'll add it. +
{getProviders(false).map((provider) => ( ["organizations"] - >(routeMatch.data.organizations); + return hydrateObject(routeMatch.data.organizations); } export function useCurrentOrganization() { @@ -26,7 +26,7 @@ export function useCurrentOrganization() { } const result = hydrateObject< - UseDataFunctionReturn["organization"] + PrismaReturnType >(routeMatch.data.organization); if (result == null) return undefined; From 35aedadfe98a8e010f9b47b7096d11c499cbd5e5 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 22:13:47 -0800 Subject: [PATCH 05/71] Revert "Merge pull request #26 from triggerdotdev/dev" This reverts commit 26c1c9f4b228933ef022e80e5fa0ae179580de08, reversing changes made to ef149bd260a313a09c90b791da1ad68f10667201. --- apps/webapp/app/utils.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/utils.ts b/apps/webapp/app/utils.ts index 4469a11ebe1..a099e857d2e 100644 --- a/apps/webapp/app/utils.ts +++ b/apps/webapp/app/utils.ts @@ -97,16 +97,11 @@ export function formatDateTime( date: Date, style: "medium" | "long" = "medium" ): string { - try { - switch (style) { - case "long": - return dateFormatterLong.format(date); - case "medium": - return dateFormatter.format(date); - } - } catch (error) { - console.error(error); - return "Unknown"; + switch (style) { + case "long": + return dateFormatterLong.format(date); + case "medium": + return dateFormatter.format(date); } } From ba617fa7d7f1a21af95ecd9eda2e19e3ec9ee68e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 22:13:50 -0800 Subject: [PATCH 06/71] Revert "Merge pull request #25 from triggerdotdev/dev" This reverts commit ef149bd260a313a09c90b791da1ad68f10667201, reversing changes made to 56e5a5c8a4b3ea94ec5d1ca053ce48181d8749a0. --- .../apis/slack/actions/post-message.mdx | 17 +---------------- .../orgs/$organizationSlug/__org/index.tsx | 7 ++----- examples/send-to-slack/src/index.tsx | 2 -- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index 3310f1fbb4b..e2689e35f05 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -127,12 +127,9 @@ In this examples there are two workflows: 1. Messages your team every minute (!) and asks them how they're doing. 2. Receives the interaction from users. It adds reactions when the buttons are pressed and posts a message when a user selects from the dropdown menu. -Please note, that to use `jsx-slack` you will need to add the `jsxImportSource` pragma to the top of your file. This is because `jsx-slack` is not a React library, but it uses the same syntax as React. Also, you might need to add `"jsx": "react-jsx"` to your `compilerOption` in `tsconfig.json`. The workflow file needs to be a `.jsx` or `.tsx` file (like when you use React). - -```typescript workflow.tsx -/** @jsxImportSource jsx-slack */ +```typescript Blocks with interactivity import { slack } from "@trigger.dev/integrations"; import JSXSlack, { Actions, @@ -210,7 +207,6 @@ new Trigger({ channelId: event.channel.id, }); } - break; } case "status-help": { //the user needs help so add an 🆘 emoji as a reaction @@ -221,7 +217,6 @@ new Trigger({ channelId: event.channel.id, }); } - break; } case "rating": { if (action.type != "static_select") { @@ -249,14 +244,4 @@ new Trigger({ }).listen(); ``` -```json tsconfig.json -{ - //other options - "compilerOptions": { - "jsx": "react-jsx" - //other options - } -} -``` - diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx index f73804499a9..c051854f187 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx @@ -184,18 +184,15 @@ function WorkflowList({ } function lastRunDescription(lastRun: WorkflowListItem["lastRun"]) { - if (lastRun === null || lastRun === undefined) { + if (lastRun === undefined) { return "Never"; } - if (lastRun.status === "SUCCESS") { if (lastRun.finishedAt) { return formatDateTime(lastRun.finishedAt); - } else { - return "Unknown"; } + throw new Error("lastRun.finishedAt is undefined"); } - return runStatusLabel(lastRun.status); } diff --git a/examples/send-to-slack/src/index.tsx b/examples/send-to-slack/src/index.tsx index 7953c4031dc..08dbcb62b7d 100644 --- a/examples/send-to-slack/src/index.tsx +++ b/examples/send-to-slack/src/index.tsx @@ -221,7 +221,6 @@ new Trigger({ channelId: event.channel.id, }); } - break; } case "status-help": { //the user needs help so add an 🆘 emoji as a reaction @@ -232,7 +231,6 @@ new Trigger({ channelId: event.channel.id, }); } - break; } case "rating": { if (action.type != "static_select") { From 416c46d4cf4444cc9523ed49f08bec2d244361b2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 22:13:54 -0800 Subject: [PATCH 07/71] Revert "Merge pull request #24 from triggerdotdev/dev" This reverts commit 56e5a5c8a4b3ea94ec5d1ca053ce48181d8749a0, reversing changes made to a1b16400d5bedad80a0ae158e9398d5dd58d82a7. --- apps/docs/examples/examples.mdx | 4 +- apps/docs/examples/resend.mdx | 5 - apps/docs/functions/send-event.mdx | 181 +--- .../apis/slack/actions/post-message.mdx | 148 +-- apps/docs/mint.json | 31 +- apps/docs/reference/send-event.mdx | 97 -- apps/docs/welcome.mdx | 10 +- .../images/triggers/slack-interaction.png | Bin 46640 -> 0 bytes .../app/components/CreateNewWorkflow.tsx | 27 +- .../webapp/app/components/UserProfileMenu.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 2 +- .../app/components/triggers/Trigger.tsx | 40 - .../app/components/triggers/TriggerIcons.tsx | 9 - .../app/components/triggers/triggerLabel.tsx | 2 - apps/webapp/app/env.server.ts | 1 - .../models/workflowListPresenter.server.ts | 56 +- apps/webapp/app/models/workflowRun.server.ts | 35 +- .../models/workflowRunListPresenter.server.ts | 8 - .../workflows/$workflowSlug/runs/$runId.tsx | 42 +- .../workflows/$workflowSlug/test.tsx | 8 +- apps/webapp/app/routes/api/v1/events.ts | 27 +- .../internal/webhooks/slack/interactivity.ts | 24 - .../$organizationSlug/test/$workflowSlug.ts | 24 +- apps/webapp/app/services/emailAuth.server.tsx | 19 +- .../app/services/events/dispatch.server.ts | 20 +- .../app/services/events/ingest.server.ts | 13 +- .../events/ingestCustomEvent.server.ts | 60 -- apps/webapp/app/services/gitHubAuth.server.ts | 11 - .../app/services/messageBroker.server.ts | 45 - .../performIntegrationRequest.server.ts | 3 - .../slack/handleInteractivity.server.ts | 73 -- .../workflows/registerWorkflow.server.ts | 62 +- apps/webapp/app/utils/formData.ts | 10 - apps/webapp/app/utils/json.ts | 7 - apps/webapp/app/utils/objects.ts | 14 - .../migration.sql | 2 - .../migration.sql | 33 - apps/webapp/prisma/schema.prisma | 39 - examples/send-to-slack/package.json | 3 +- examples/send-to-slack/src/index.ts | 39 + examples/send-to-slack/src/index.tsx | 281 ------ examples/send-to-slack/tsconfig.json | 8 +- examples/smoke-test/src/index.ts | 20 - flightcontrol.json | 8 +- packages/common-schemas/src/events.ts | 27 - packages/common-schemas/src/triggers.ts | 18 +- .../src/services/index.ts | 37 +- .../internal-integrations/src/slack/index.ts | 247 ++++- .../src/slack/requests.ts | 467 ---------- packages/internal-integrations/src/types.ts | 1 - packages/trigger-integrations/CHANGELOG.md | 10 - packages/trigger-integrations/package.json | 2 +- .../src/integrations/slack/events.ts | 37 - .../src/integrations/slack/index.ts | 64 -- packages/trigger-providers/CHANGELOG.md | 6 - packages/trigger-providers/package.json | 3 +- .../src/providers/slack/blocks.ts | 861 ------------------ .../src/providers/slack/index.ts | 2 - .../src/providers/slack/interactivity.ts | 215 ----- .../src/providers/slack/prodmanifest.json | 6 +- .../src/providers/slack/schemas.ts | 48 +- packages/trigger-providers/src/schemaUtils.ts | 86 -- packages/trigger-sdk/CHANGELOG.md | 7 - packages/trigger-sdk/package.json | 2 +- pnpm-lock.yaml | 245 ++++- 65 files changed, 608 insertions(+), 3336 deletions(-) delete mode 100644 apps/docs/reference/send-event.mdx delete mode 100644 apps/webapp/app/assets/images/triggers/slack-interaction.png delete mode 100644 apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts delete mode 100644 apps/webapp/app/services/events/ingestCustomEvent.server.ts delete mode 100644 apps/webapp/app/services/slack/handleInteractivity.server.ts delete mode 100644 apps/webapp/app/utils/formData.ts delete mode 100644 apps/webapp/app/utils/json.ts delete mode 100644 apps/webapp/app/utils/objects.ts delete mode 100644 apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql delete mode 100644 apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql create mode 100644 examples/send-to-slack/src/index.ts delete mode 100644 examples/send-to-slack/src/index.tsx delete mode 100644 packages/internal-integrations/src/slack/requests.ts delete mode 100644 packages/trigger-integrations/src/integrations/slack/events.ts delete mode 100644 packages/trigger-providers/src/providers/slack/blocks.ts delete mode 100644 packages/trigger-providers/src/providers/slack/interactivity.ts delete mode 100644 packages/trigger-providers/src/schemaUtils.ts diff --git a/apps/docs/examples/examples.mdx b/apps/docs/examples/examples.mdx index e1addc36d8c..c8196f5e109 100644 --- a/apps/docs/examples/examples.mdx +++ b/apps/docs/examples/examples.mdx @@ -17,7 +17,5 @@ You can use these workflows in your product by following the instructions on eac Post to Slack when a GitHub issue is created or modified. - - Create a welcome email drip campaign. - + diff --git a/apps/docs/examples/resend.mdx b/apps/docs/examples/resend.mdx index 36ce42ac255..8e3d744d306 100644 --- a/apps/docs/examples/resend.mdx +++ b/apps/docs/examples/resend.mdx @@ -4,11 +4,6 @@ sidebarTitle: "Resend" description: "Here are some example workflows you could create using the Resend integration." --- - - Resend is in private beta, if you would like access please contact us at{" "} - help@trigger.dev - - ## Welcome email drip campaign This workflow will get triggered and a Slack notification and welcom email will be sent straight away. diff --git a/apps/docs/functions/send-event.mdx b/apps/docs/functions/send-event.mdx index 9c11909a576..b709fcaaae7 100644 --- a/apps/docs/functions/send-event.mdx +++ b/apps/docs/functions/send-event.mdx @@ -4,12 +4,9 @@ sidebarTitle: "Send event" description: "Send events to trigger your custom events workflows." --- -## Send an event +## How to send an event -You can easily send an event from anywhere, including from inside another workflow. Events don't have to come from the same server as your workflow and can be sent as HTTP requests from any language (see our cURL example below for the format). - - - +You can send an event from anywhere in your code, including from inside another workflow. ```ts import { sendEvent } from "@trigger.dev/sdk"; @@ -22,180 +19,12 @@ await sendEvent("start-fire-2", { }); ``` - - - -```bash -curl --request POST \ - --url https://app.trigger.dev/api/v1/events \ - --header 'Authorization: Bearer ' \ - --header 'Content-Type: application/json' \ - --data '{ - "id": "", - "event": { - "name": "user.created", - "payload": { - "userId": "123456" - } - } - }' -``` - - - - If you are calling this from inside a workflow, ensure that the first parameter is unique inside your workflow. ## Sending events from other workflows -It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them. The below example shows how a scheduled workflow delegates the work via sending a custom event: - - - -```ts check-scheduler.ts -new Trigger({ - id: "check-scheduler", - name: "Check Scheduler", - on: scheduleEvent({ rateOf: { minutes: 10 } }), - run: async (event, context) => { - await context.sendEvent("health.check trigger.dev", { - name: "health.check", - payload: { - url: "https://trigger.dev", - host: "trigger.dev", - }, - }); - - await context.sendEvent("health.check docs.trigger.dev", { - name: "health.check", - payload: { - url: "https://docs.trigger.dev", - host: "docs.trigger.dev", - }, - }); - - await context.sendEvent("health.check app.trigger.dev", { - name: "health.check", - payload: { - url: "https://app.trigger.dev/healthcheck", - host: "app.trigger.dev", - }, - }); - }, -}).listen(); -``` - -```ts health-check.ts -export const healthCheck = new Trigger({ - id: "health-check", - name: "Health Check", - on: customEvent({ - name: "health.check", - schema: z.object({ - url: z.string().url(), - host: z.string(), - }), - }), - run: async (event, context) => { - const response = await context.fetch("fetch site", event.url, { - method: "GET", - retry: { - enabled: false, - }, - }); - - if (response.ok) { - await context.logger.info(`${event.host} is up!`); - return; - } - - await slack.postMessage("Site is down", { - channelName: "health-checks", - text: `${event.host} is down: ${response.status}`, - }); - }, -}).listen(); -``` - - - -## Delaying event delivery +It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them. -Through both our Node.js SDK and our HTTP API, you can delay the delivery of an event. This is useful if you want to send an event to trigger a workflow, but you want to wait for a certain amount of time before the event is delivered. +## Writing a workflow that is triggered by an event -Using the health check example above, we can delay the delivery of the `health.check` event in various ways: - -```ts -new Trigger({ - id: "check-scheduler", - name: "Check Scheduler", - on: scheduleEvent({ rateOf: { minutes: 10 } }), - run: async (event, context) => { - await context.sendEvent("health.check trigger.dev", { - name: "health.check", - payload: { - url: "https://trigger.dev", - host: "trigger.dev", - }, - delay: { until: new Date(Date.now() + 1000 * 60 * 5) }, // Delay until a specific date - }); - - await context.sendEvent("health.check docs.trigger.dev", { - name: "health.check", - payload: { - url: "https://docs.trigger.dev", - host: "docs.trigger.dev", - }, - delay: { minutes: 5 }, // Delay for a specific amount of time - }); - - await context.sendEvent("health.check app.trigger.dev", { - name: "health.check", - payload: { - url: "https://app.trigger.dev/healthcheck", - host: "app.trigger.dev", - }, - delay: { seconds: 30 }, // Delay for a specific amount of time - }); - }, -}).listen(); -``` - - - When sending events in the context of a workflow run, adding a delay DOES NOT - add a delay to the workflow run, as sending a custom event is a - fire-and-forget action. - - -You can add delays via the HTTP API by adding a `delay` object to the event: - -```bash -curl --request POST \ - --url https://app.trigger.dev/api/v1/events \ - --header 'Authorization: Bearer ' \ - --header 'Content-Type: application/json' \ - --data '{ - "id": "", - "event": { - "name": "user.created", - "payload": { - "userId": "123456" - }, - "delay": { - "seconds": 30 - } - } - }' -``` - -## Event deduplication - -When sending events, you can specify an `id` for the event. If you send an event with the same `id` multiple times, only the first event will be delivered. This is useful if you want to ensure that an event is only delivered once, even if you send it multiple times. - -```ts -sendEvent({ - id: "my-event-id", - name: "my-event", - payload: { foo: "bar" }, -}); -``` +You should read [the documentation on custom events](/triggers/custom-events) to learn how to write a workflow that is triggered by an event. diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index e2689e35f05..e1e9585a583 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -28,14 +28,6 @@ Publish slack messages to a public or private channel in your Slack Workspace as The formatted text of the message to be published, formatted as [mrkdwn](https://api.slack.com/reference/surfaces/formatting#basics). - - You can use blocks to create a rich message with images, buttons, and more. - - You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#) to see it in action. - - Full full detail on block see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). - - @@ -77,8 +69,6 @@ Publish slack messages to a public or private channel in your Slack Workspace as ## Example Workflows -### Notify Slack on New Star - ```typescript Notify Slack on New Star @@ -99,7 +89,11 @@ new Trigger({ }).listen(); ``` -```json Example response + + +## Example Response + +```json { "ok": true, "ts": "1673618429.084699", @@ -113,135 +107,3 @@ new Trigger({ } } ``` - - - -### Blocks with interactivity - -You can add interaction and richer visual elements using Blocks. The easiest way to add them is using `jsx-slack` (which is a separate npm package that you can install). - -This example messages your team every minute (!) and asks them how they're doing. There are two buttons and a dropdown menu. - -In this examples there are two workflows: - -1. Messages your team every minute (!) and asks them how they're doing. -2. Receives the interaction from users. It adds reactions when the buttons are pressed and posts a message when a user selects from the dropdown menu. - - - -```typescript Blocks with interactivity -import { slack } from "@trigger.dev/integrations"; -import JSXSlack, { - Actions, - Blocks, - Button, - Section, - Select, - Option, -} from "jsx-slack"; -import { z } from "zod"; - -const BLOCK_ID = "issue.action.block"; - -//1. every minute see how your employees are doing, we don't recommend this frequency 😉 -new Trigger({ - id: "slack-interactivity", - name: "Testing Slack Interactivity", - on: scheduleEvent({ - rateOf: { - minutes: 1, - }, - }), - run: async (event, ctx) => { - await slack.postMessage("jsx-test", { - channelName: "test-integrations", - //text appears in Slack notifications on mobile/desktop - text: "How is your progress today?", - //import and use JSXSlack to make creating rich messages much easier - blocks: JSXSlack( - -
How is your progress today?
- - - - - -
- ), - }); - }, -}).listen(); - -//2. this workflow listens for Slack interactions filtered by the block id and actions we used above -new Trigger({ - id: "slack-block-interaction", - name: "Slack Block Interaction", - on: slack.events.blockActionInteraction({ - blockId: BLOCK_ID, - actionId: ["status-blocked", "status-help", "rating"], - }), - run: async (event, ctx) => { - //create promises from all the actions - const promises = event.actions.map((action) => { - switch (action.action_id) { - case "status-blocked": { - //the user is blocked so add a 😢 emoji as a reaction - if (event.message) { - return slack.addReaction("React to message", { - name: "cry", - timestamp: event.message.ts, - channelId: event.channel.id, - }); - } - } - case "status-help": { - //the user needs help so add an 🆘 emoji as a reaction - if (event.message) { - return slack.addReaction("React to message", { - name: "sos", - timestamp: event.message.ts, - channelId: event.channel.id, - }); - } - } - case "rating": { - if (action.type != "static_select") { - throw new Error("This action should be a select"); - } - - //post the rating as a message that appears below the original, - //only the user pressing the button will see this message - return slack.postMessageResponse( - "Added a comment to the issue", - event.response_url, - { - text: `You rated your day ${action.selected_option?.value} stars`, - replace_original: false, - } - ); - } - default: - return Promise.resolve(); - } - }); - - await Promise.all(promises); - }, -}).listen(); -``` - -
diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 77f128cced6..41e403a370c 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -3,7 +3,7 @@ "logo": { "dark": "/logo/light.png", "light": "/logo/light.png", - "href": "https://app.trigger.dev" + "href": "https://trigger.dev" }, "favicon": "/images/favicon.png", "colors": { @@ -20,16 +20,6 @@ "default": "dark", "isHidden": true }, - "topbarLinks": [ - { - "name": "Login", - "url": "https://app.trigger.dev" - }, - { - "name": "Sign up", - "url": "https://app.trigger.dev" - } - ], "topbarCtaButton": { "type": "github", "url": "https://github.com/triggerdotdev/trigger.dev" @@ -49,7 +39,11 @@ "navigation": [ { "group": "Getting Started", - "pages": ["welcome", "getting-started", "get-help"] + "pages": [ + "welcome", + "getting-started", + "get-help" + ] }, { "group": "Examples", @@ -77,11 +71,15 @@ "pages": [ { "group": "Slack", - "pages": ["integrations/apis/slack/actions/post-message"] + "pages": [ + "integrations/apis/slack/actions/post-message" + ] }, { "group": "Resend.com", - "pages": ["integrations/apis/resend/actions/send-email"] + "pages": [ + "integrations/apis/resend/actions/send-email" + ] } ] }, @@ -113,8 +111,7 @@ "reference/trigger", "reference/custom-event", "reference/webhook-event", - "reference/schedule-event", - "reference/send-event" + "reference/schedule-event" ] }, { @@ -140,4 +137,4 @@ "github": "https://github.com/triggerdotdev/trigger.dev", "discord": "https://discord.gg/nkqV9xBYWy" } -} +} \ No newline at end of file diff --git a/apps/docs/reference/send-event.mdx b/apps/docs/reference/send-event.mdx deleted file mode 100644 index 06c931d9f0f..00000000000 --- a/apps/docs/reference/send-event.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: "sendEvent" -sidebarTitle: "sendEvent" -description: "Send a custom event to Trigger, from inside or outside of a workflow." ---- - -## Usage - -```ts -import { Trigger, customEvent } from "@trigger.dev/sdk"; - -new Trigger({ - id: "usage", - name: "usage", - on: customEvent({ - name: "user.created", - schema: z.any(), - }), - run: async (event, ctx) => { - await ctx.sendEvent("Sending context user.created event", { - name: "user.created", - payload: { - id: "1234_abc", - }, - }); - }, -}).listen(); -``` - -You can also use `sendEvent` by importing it from `@trigger.dev/sdk`: - -```ts -import { sendEvent } from "@trigger.dev/sdk"; - -await sendEvent("Sending context user.created event", { - name: "user.created", - payload: { - id: "1234_abc", - }, -}); -``` - - - If calling `sendEvent` from outside of a workflow run, it will make an HTTP - API request to app.trigger.dev to send the event. Make sure you set the - `TRIGGER_API_KEY` environment variable if you will be using it this way. - - -## Parameters - - - A unique key for the event in the context of a workflow run. Please see the - [Keys and Resumability](/guides/resumability) guide for more info. - - - - - - An optional unique ID for the event. If not provided, one will be - generated automatically (using `clid`). Set this field to perform event - deduplication. - - - - The name of the event. This is the name you set when creating the - `customEvent` trigger. - - - - The payload of the event. - - - - An optional timestamp for the event. If not provided, one will be generated. Must be in ISO 8601 format (e.g. `new Date().toISOString()`) - - - - An optional context object for the event. This can be used to pass - additional information about the event, such as the user who triggered - it. Note that this is not currently exposed to the workflow (coming soon). - - - - An optional delay object for the event. This can be used to delay the - event by a certain amount of time. Use one of the following options: - - - `{ seconds: number }` - delay the event by a certain number of seconds - - `{ minutes: number }` - delay the event by a certain number of minutes - - `{ hours: number }` - delay the event by a certain number of hours - - `{ days: number }` - delay the event by a certain number of days - - `{ until: Date }` - delay the event until a certain date - - See the [Delaying Event Delivery](/functions/send-event#delaying-event-delivery) docs for more info. - - - - diff --git a/apps/docs/welcome.mdx b/apps/docs/welcome.mdx index c723b28780f..98c6b3043e9 100644 --- a/apps/docs/welcome.mdx +++ b/apps/docs/welcome.mdx @@ -8,21 +8,19 @@ Trigger.dev is an open source platform that enables developers to create event-d Workflows live in your codebase so you can use your existing types, functions, version control and IDE. They are triggered by us but run on your server so your private data is never exposed. -Go to [trigger.dev](https://app.trigger.dev) and sign up or login to your account. - _A workflow in action:_ ![Test a run](/images/workflow-demo.gif) -  + Quickly get up and running with Trigger.dev by following our quick start diff --git a/apps/webapp/app/assets/images/triggers/slack-interaction.png b/apps/webapp/app/assets/images/triggers/slack-interaction.png deleted file mode 100644 index 9ea87cfe05e4d936cf34aa6ad94e68aa8ac0d0fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46640 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%0A2!=6iGfa+DNUZoa@TcbcesG%D; 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 diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 591f8337cea..92961e3930b 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -62,7 +62,7 @@ export function CreateNewWorkflowNoWorkflows() { diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx index f2ba3ac848d..f17914345af 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx @@ -18,15 +18,11 @@ import { requireUserId } from "~/services/session.server"; export const loader = async ({ request, params }: LoaderArgs) => { await requireUserId(request); - const { workflowSlug, organizationSlug } = params; + const { workflowSlug } = params; invariant(workflowSlug, "workflowSlug is required"); - invariant(organizationSlug, "organizationSlug is required"); try { - const latestRun = await getMostRecentWorkflowRun({ - workflowSlug, - organizationSlug, - }); + const latestRun = await getMostRecentWorkflowRun({ workflowSlug }); return typedjson({ latestRun }); } catch (error: any) { console.error(error); diff --git a/apps/webapp/app/routes/api/v1/events.ts b/apps/webapp/app/routes/api/v1/events.ts index 7f1ac0c42ce..062d6aaf949 100644 --- a/apps/webapp/app/routes/api/v1/events.ts +++ b/apps/webapp/app/routes/api/v1/events.ts @@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime"; import { CustomEventSchema } from "@trigger.dev/common-schemas"; import { z } from "zod"; import { authenticateApiRequest } from "~/services/apiAuth.server"; -import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server"; +import { IngestEvent } from "~/services/events/ingest.server"; const EventBodySchema = z.object({ id: z.string(), @@ -32,13 +32,20 @@ export async function action({ request }: ActionArgs) { return json({ error: eventBody.error.message }, { status: 400 }); } - const service = new IngestCustomEvent(); - - await service.call({ - id: eventBody.data.id, - event: eventBody.data.event, - apiKey: authenticatedEnv.apiKey, - }); - - return { status: 200 }; + const service = new IngestEvent(); + + const result = await service.call( + { + id: eventBody.data.id, + name: eventBody.data.event.name, + type: "CUSTOM_EVENT", + service: "trigger", + payload: eventBody.data.event.payload, + context: eventBody.data.event.context, + apiKey: authenticatedEnv.apiKey, + }, + authenticatedEnv.organization + ); + + return json(result.data); } diff --git a/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts deleted file mode 100644 index f3c988ef7e2..00000000000 --- a/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ActionArgs } from "@remix-run/server-runtime"; -import { HandleSlackInteractivity } from "~/services/slack/handleInteractivity.server"; - -export async function action({ request }: ActionArgs) { - const formData = await request.formData(); - - const payload = formData.get("payload"); - - if (typeof payload !== "string") { - return { status: 400 }; - } - - const parsedPayload = JSON.parse(payload); - - const service = new HandleSlackInteractivity(); - - try { - await service.call(parsedPayload); - } catch (error) { - console.error(error); - } - - return { status: 200 }; -} diff --git a/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts b/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts index 9ddc914c4ef..30cd55775e0 100644 --- a/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts +++ b/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts @@ -13,7 +13,6 @@ import { import { getWorkflowFromSlugs } from "~/models/workflow.server"; import { CreateWorkflowTestRun } from "~/services/runs/createTestRun.server"; import { requireUserId } from "~/services/session.server"; -import { safeJsonParse } from "~/utils/json"; const requestSchema = z.object({ eventName: z.string(), @@ -40,15 +39,7 @@ export const action = async ({ request, params }: ActionArgs) => { const body = Object.fromEntries(formData.entries()); const { eventName, payload, source } = requestSchema.parse(body); - const jsonPayload = safeJsonParse(payload); - - if (!jsonPayload) { - return redirectWithErrorMessage( - redirectUriForSource(source, organizationSlug, workflowSlug), - request, - "Invalid JSON payload" - ); - } + const jsonPayload = JSON.parse(payload); const workflow = await getWorkflowFromSlugs({ userId, @@ -107,19 +98,6 @@ export const action = async ({ request, params }: ActionArgs) => { } }; -function redirectUriForSource( - source: "rerun" | "test", - organizationSlug: string, - workflowSlug: string, - runId?: string -) { - if (source === "rerun") { - return `/orgs/${organizationSlug}/workflows/${workflowSlug}/runs/${runId}`; - } else { - return `/orgs/${organizationSlug}/workflows/${workflowSlug}/test`; - } -} - function errorMessageForSource(source: "rerun" | "test") { if (source === "rerun") { return "Unable to rerun this workflow. Please contact help@trigger.dev for assistance."; diff --git a/apps/webapp/app/services/emailAuth.server.tsx b/apps/webapp/app/services/emailAuth.server.tsx index 23af9558570..83785dff738 100644 --- a/apps/webapp/app/services/emailAuth.server.tsx +++ b/apps/webapp/app/services/emailAuth.server.tsx @@ -6,7 +6,6 @@ import { findOrCreateUser } from "~/models/user.server"; import { env } from "~/env.server"; import { createFirstOrganization } from "~/models/organization.server"; import { sendMagicLinkEmail } from "~/services/email.server"; -import { taskQueue } from "./messageBroker.server"; let secret = env.MAGIC_LINK_SECRET; if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable."); @@ -33,19 +32,15 @@ const emailStrategy = new EmailLinkStrategy( authenticationMethod: "MAGIC_LINK", }); + console.log( + `User ${user.id} logged in with magic link. ${ + isNewUser ? "New user." : "" + }` + ); + if (isNewUser) { await createFirstOrganization(user); - await emailProvider.scheduleWelcomeEmail(user); - - await taskQueue.publish("SEND_INTERNAL_EVENT", { - id: user.id, - name: "user.created", - payload: { - id: user.id, - source: "MAGIC_LINK", - admin: user.admin, - }, - }); + emailProvider.scheduleWelcomeEmail(user); } return { userId: user.id }; diff --git a/apps/webapp/app/services/events/dispatch.server.ts b/apps/webapp/app/services/events/dispatch.server.ts index f547d2efca6..7aa3366a96a 100644 --- a/apps/webapp/app/services/events/dispatch.server.ts +++ b/apps/webapp/app/services/events/dispatch.server.ts @@ -127,22 +127,14 @@ class EventMatcher { } function patternMatches(payload: any, pattern: any): boolean { - for (const [patternKey, patternValue] of Object.entries(pattern)) { - const payloadValue = payload[patternKey]; - - if (Array.isArray(patternValue)) { - if (patternValue.length > 0 && !patternValue.includes(payloadValue)) { + for (const [key, value] of Object.entries(pattern)) { + if (Array.isArray(value)) { + if (!value.includes(payload[key])) { return false; } - } else if (typeof patternValue === "object") { - if (Array.isArray(payloadValue)) { - if (!payloadValue.some((item) => patternMatches(item, patternValue))) { - return false; - } - } else { - if (!patternMatches(payloadValue, patternValue)) { - return false; - } + } else if (typeof value === "object") { + if (!patternMatches(payload[key], value)) { + return false; } } } diff --git a/apps/webapp/app/services/events/ingest.server.ts b/apps/webapp/app/services/events/ingest.server.ts index 6ed3c602020..b3ba11f1ccf 100644 --- a/apps/webapp/app/services/events/ingest.server.ts +++ b/apps/webapp/app/services/events/ingest.server.ts @@ -25,29 +25,20 @@ export class IngestEvent { this.#prismaClient = prismaClient; } - public async call(options: IngestEventOptions, organization?: Organization) { + public async call(options: IngestEventOptions, organization: Organization) { const environment = options.apiKey ? await findEnvironmentByApiKey(options.apiKey) : undefined; - if (!environment && !organization) { - return { - status: "error" as const, - error: "No organization or environment found", - }; - } - const id = options.id ?? ulid(); - const organizationId = organization?.id ?? environment?.organizationId; - // Create a new event in the database const event = await this.#prismaClient.triggerEvent.create({ data: { id, organization: { connect: { - id: organizationId, + id: organization.id, }, }, environment: environment diff --git a/apps/webapp/app/services/events/ingestCustomEvent.server.ts b/apps/webapp/app/services/events/ingestCustomEvent.server.ts deleted file mode 100644 index 06673e7762c..00000000000 --- a/apps/webapp/app/services/events/ingestCustomEvent.server.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { CustomEventSchema } from "@trigger.dev/common-schemas"; -import type { PublishOptions } from "internal-platform"; -import { ulid } from "ulid"; -import type { z } from "zod"; -import { taskQueue } from "~/services/messageBroker.server"; -import { omit } from "~/utils/objects"; -import { IngestEvent } from "./ingest.server"; - -export type IngestCustomEventOptions = { - id?: string; - apiKey: string; - event: z.infer; - isTest?: boolean; -}; - -export class IngestCustomEvent { - public async call(options: IngestCustomEventOptions) { - if (options.event.delay) { - const deliveryOptions: PublishOptions = - "until" in options.event.delay - ? { deliverAt: new Date(options.event.delay.until).getTime() } - : "seconds" in options.event.delay - ? { deliverAfter: options.event.delay.seconds * 1000 } - : "minutes" in options.event.delay - ? { deliverAfter: options.event.delay.minutes * 60 * 1000 } - : "hours" in options.event.delay - ? { deliverAfter: options.event.delay.hours * 60 * 60 * 1000 } - : "days" in options.event.delay - ? { deliverAfter: options.event.delay.days * 60 * 60 * 24 * 1000 } - : { deliverAfter: 0 }; - - await taskQueue.publish( - "INGEST_DELAYED_EVENT", - { - id: options.id, - apiKey: options.apiKey, - event: omit(options.event, ["delay"]), - }, - {}, - deliveryOptions - ); - - return; - } - - const ingestService = new IngestEvent(); - - await ingestService.call({ - id: options.id ?? ulid(), - name: options.event.name, - type: "CUSTOM_EVENT", - service: "trigger", - payload: options.event.payload, - context: options.event.context, - timestamp: options.event.timestamp, - apiKey: options.apiKey, - isTest: options.isTest, - }); - } -} diff --git a/apps/webapp/app/services/gitHubAuth.server.ts b/apps/webapp/app/services/gitHubAuth.server.ts index 7b5b1a5cc57..a32f3af627e 100644 --- a/apps/webapp/app/services/gitHubAuth.server.ts +++ b/apps/webapp/app/services/gitHubAuth.server.ts @@ -5,7 +5,6 @@ import { createFirstOrganization } from "~/models/organization.server"; import { findOrCreateUser } from "~/models/user.server"; import type { AuthUser } from "./authUser"; import { scheduleWelcomeEmail } from "./email.server"; -import { taskQueue } from "./messageBroker.server"; const gitHubStrategy = new GitHubStrategy( { @@ -32,16 +31,6 @@ const gitHubStrategy = new GitHubStrategy( if (isNewUser) { await createFirstOrganization(user); await scheduleWelcomeEmail(user); - - await taskQueue.publish("SEND_INTERNAL_EVENT", { - id: user.id, - name: "user.created", - payload: { - id: user.id, - source: "GITHUB", - admin: user.admin, - }, - }); } return { diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts index 1d4f04cc161..2e504053017 100644 --- a/apps/webapp/app/services/messageBroker.server.ts +++ b/apps/webapp/app/services/messageBroker.server.ts @@ -1,6 +1,5 @@ import type { FetchOutputSchema } from "@trigger.dev/common-schemas"; import { - CustomEventSchema, JsonSchema, ScheduledEventPayloadSchema, } from "@trigger.dev/common-schemas"; @@ -35,7 +34,6 @@ import { InitiateDelay } from "./delays/initiateDelay.server"; import { ResolveDelay } from "./delays/resolveDelay.server"; import { sendEmail } from "./email.server"; import { DispatchEvent } from "./events/dispatch.server"; -import { IngestCustomEvent } from "./events/ingestCustomEvent.server"; import { HandleNewServiceConnection } from "./externalServices/handleNewConnection.server"; import { RegisterExternalSource } from "./externalSources/registerExternalSource.server"; import { CreateFetchRequest } from "./fetches/createFetchRequest.server"; @@ -51,7 +49,6 @@ import { WorkflowRunDisconnected } from "./runs/runDisconnected.server"; import { WorkflowRunTriggerTimeout } from "./runs/runTriggerTimeout.server"; import { DeliverScheduledEvent } from "./scheduler/deliverScheduledEvent.server"; import { RegisterSchedulerSource } from "./scheduler/registerSchedulerSource.server"; -import { omit } from "~/utils/objects"; let pulsarClient: PulsarClient; let triggerPublisher: ZodPublisher; @@ -375,14 +372,6 @@ const taskQueueCatalog = { data: z.object({ id: z.string() }), properties: z.object({}), }, - INGEST_DELAYED_EVENT: { - data: z.object({ - id: z.string().optional(), - apiKey: z.string(), - event: CustomEventSchema.omit({ delay: true }), - }), - properties: z.object({}), - }, TRIGGER_WORKFLOW_RUN: { data: z.object({ id: z.string() }), properties: z.object({}), @@ -430,10 +419,6 @@ const taskQueueCatalog = { }), properties: z.object({}), }, - SEND_INTERNAL_EVENT: { - data: CustomEventSchema.extend({ id: z.string() }), - properties: z.object({}), - }, }; function createTaskQueue() { @@ -655,17 +640,6 @@ function createTaskQueue() { return true; }, - INGEST_DELAYED_EVENT: async (id, data, properties, attributes) => { - if (attributes.redeliveryCount >= 4) { - return true; - } - - const ingestService = new IngestCustomEvent(); - - await ingestService.call(data); - - return true; - }, TRIGGER_WORKFLOW_RUN: async (id, data, properties) => { const run = await findWorklowRunById(data.id); @@ -705,25 +679,6 @@ function createTaskQueue() { return service.call(data.externalSourceId, data.payload); }, - SEND_INTERNAL_EVENT: async (id, data, properties, attributes) => { - if (attributes.redeliveryCount >= 4) { - return true; - } - - if (!env.INTERNAL_TRIGGER_API_KEY) { - return true; - } - - const service = new IngestCustomEvent(); - - await service.call({ - id: data.id, - event: omit(data, ["id"]), - apiKey: env.INTERNAL_TRIGGER_API_KEY, - }); - - return true; - }, }, }); diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index 0c2275dfc9b..320042e1fd5 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -231,7 +231,6 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, - metadata: { requestId: integrationRequest.id }, }); } case "shopify": { @@ -240,7 +239,6 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, - metadata: { requestId: integrationRequest.id }, }); } case "resend": { @@ -249,7 +247,6 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, - metadata: { requestId: integrationRequest.id }, }); } default: { diff --git a/apps/webapp/app/services/slack/handleInteractivity.server.ts b/apps/webapp/app/services/slack/handleInteractivity.server.ts deleted file mode 100644 index 1d3b55c38a2..00000000000 --- a/apps/webapp/app/services/slack/handleInteractivity.server.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { slack } from "@trigger.dev/providers"; -import { ulid } from "ulid"; -import { generateErrorMessage } from "zod-error"; -import type { PrismaClient } from "~/db.server"; -import { prisma } from "~/db.server"; -import { IngestEvent } from "../events/ingest.server"; - -export class HandleSlackInteractivity { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call(payload: unknown) { - console.log("payload", JSON.stringify(payload, null, 2)); - - const parsedPayload = slack.schemas.blockAction.safeParse(payload); - - if (!parsedPayload.success) { - console.error( - "Invalid payload", - generateErrorMessage(parsedPayload.error.issues) - ); - - return; - } - - if (parsedPayload.data.type !== "block_actions") { - return; - } - - if (!parsedPayload.data.message) { - return; - } - - if (!parsedPayload.data.message.metadata) { - return; - } - - const { requestId } = parsedPayload.data.message.metadata.event_payload; - - const integrationRequest = - await this.#prismaClient.integrationRequest.findUnique({ - where: { id: requestId }, - include: { - run: { - include: { - environment: true, - workflow: true, - }, - }, - }, - }); - - if (!integrationRequest) { - return; - } - - const ingestService = new IngestEvent(); - - await ingestService.call({ - id: ulid(), - type: "SLACK_INTERACTION", - name: "block.action", - service: "slack", - payload: parsedPayload.data, - apiKey: integrationRequest.run.environment.apiKey, - }); - - return true; - } -} diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts index 90da212f76b..d4b027d629c 100644 --- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts +++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts @@ -45,23 +45,12 @@ export class RegisterWorkflow { } if (validation.data.trigger.service !== "trigger") { - const source = await this.upsertSource( + await this.#upsertExternalSource( validation.data, organization, workflow, environment ); - - if (source && source.status === "READY") { - await this.#prismaClient.workflow.update({ - where: { - id: workflow.id, - }, - data: { - status: "READY", - }, - }); - } } await this.#upsertEventRule( @@ -110,18 +99,6 @@ export class RegisterWorkflow { payload: WorkflowMetadata, organization: Organization ) { - const existingWorkflow = await this.#prismaClient.workflow.findUnique({ - where: { - organizationId_slug: { - organizationId: organization.id, - slug, - }, - }, - select: { - id: true, - }, - }); - const workflow = await this.#prismaClient.workflow.upsert({ where: { organizationId_slug: { @@ -153,20 +130,10 @@ export class RegisterWorkflow { }, }); - if (!existingWorkflow) { - await taskQueue.publish("SEND_INTERNAL_EVENT", { - id: workflow.id, - name: "workflow.created", - payload: { - id: workflow.id, - }, - }); - } - return workflow; } - async upsertSource( + async #upsertExternalSource( payload: WorkflowMetadata, organization: Organization, workflow: Workflow, @@ -231,31 +198,6 @@ export class RegisterWorkflow { return schedulerSource; } - case "SLACK_INTERACTION": { - if (!payload.trigger.source) { - return; - } - - return this.#prismaClient.internalSource.upsert({ - where: { - workflowId_environmentId: { - workflowId: workflow.id, - environmentId: environment.id, - }, - }, - update: { - source: payload.trigger.source, - }, - create: { - organizationId: organization.id, - workflowId: workflow.id, - environmentId: environment.id, - source: payload.trigger.source, - status: "READY", - type: "SLACK", - }, - }); - } default: { return; } diff --git a/apps/webapp/app/utils/formData.ts b/apps/webapp/app/utils/formData.ts deleted file mode 100644 index 8290365263a..00000000000 --- a/apps/webapp/app/utils/formData.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { z } from "zod"; - -export function formDataAsObject( - formData: FormData, - schema: z.ZodSchema -): TValues { - const object = Object.fromEntries(formData.entries()); - const parsed = schema.parse(object); - return parsed; -} diff --git a/apps/webapp/app/utils/json.ts b/apps/webapp/app/utils/json.ts deleted file mode 100644 index 2da50949c96..00000000000 --- a/apps/webapp/app/utils/json.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function safeJsonParse(json: string): unknown { - try { - return JSON.parse(json); - } catch (e) { - return null; - } -} diff --git a/apps/webapp/app/utils/objects.ts b/apps/webapp/app/utils/objects.ts deleted file mode 100644 index 337fba9cac6..00000000000 --- a/apps/webapp/app/utils/objects.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function omit, K extends keyof T>( - obj: T, - keys: K[] -): Omit { - const result: any = {}; - - for (const key of Object.keys(obj)) { - if (!keys.includes(key as K)) { - result[key] = obj[key]; - } - } - - return result; -} diff --git a/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql b/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql deleted file mode 100644 index e9b9eceef04..00000000000 --- a/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterEnum -ALTER TYPE "TriggerType" ADD VALUE 'SLACK_INTERACTION'; diff --git a/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql b/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql deleted file mode 100644 index 2bbe8c1cba7..00000000000 --- a/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql +++ /dev/null @@ -1,33 +0,0 @@ --- CreateEnum -CREATE TYPE "InternalSourceType" AS ENUM ('SLACK'); - --- CreateEnum -CREATE TYPE "InternalSourceStatus" AS ENUM ('CREATED', 'READY', 'CANCELLED'); - --- CreateTable -CREATE TABLE "InternalSource" ( - "id" TEXT NOT NULL, - "organizationId" TEXT NOT NULL, - "workflowId" TEXT NOT NULL, - "environmentId" TEXT NOT NULL, - "type" "InternalSourceType" NOT NULL, - "source" JSONB NOT NULL, - "status" "InternalSourceStatus" NOT NULL DEFAULT 'CREATED', - "readyAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "InternalSource_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "InternalSource_workflowId_environmentId_key" ON "InternalSource"("workflowId", "environmentId"); - --- AddForeignKey -ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "Workflow"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "RuntimeEnvironment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma index ca286c50ced..a3b0319619e 100644 --- a/apps/webapp/prisma/schema.prisma +++ b/apps/webapp/prisma/schema.prisma @@ -52,7 +52,6 @@ model Organization { externalSources ExternalSource[] eventRules EventRule[] schedulerSources SchedulerSource[] - internalSources InternalSource[] } model APIConnection { @@ -108,7 +107,6 @@ model RuntimeEnvironment { runs WorkflowRun[] eventRules EventRule[] schedulerSources SchedulerSource[] - internalSources InternalSource[] @@unique([organizationId, slug]) } @@ -136,7 +134,6 @@ model Workflow { rules EventRule[] externalServices ExternalService[] schedulerSources SchedulerSource[] - internalSources InternalSource[] service String @default("trigger") eventNames String[] @@ -184,7 +181,6 @@ enum TriggerType { HTTP_ENDPOINT EVENT_BRIDGE HTTP_POLLING - SLACK_INTERACTION } enum WorkflowStatus { @@ -261,41 +257,6 @@ enum SchedulerSourceStatus { CANCELLED } -// We should eventually move SchedulerSource to this as it's more generic -model InternalSource { - id String @id @default(cuid()) - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade, onUpdate: Cascade) - workflowId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - type InternalSourceType - source Json - - status InternalSourceStatus @default(CREATED) - - readyAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([workflowId, environmentId]) -} - -enum InternalSourceType { - SLACK -} - -enum InternalSourceStatus { - CREATED - READY - CANCELLED -} - model ExternalService { id String @id @default(cuid()) slug String diff --git a/examples/send-to-slack/package.json b/examples/send-to-slack/package.json index 1f261d4ee88..93f35229e02 100644 --- a/examples/send-to-slack/package.json +++ b/examples/send-to-slack/package.json @@ -6,7 +6,6 @@ "dependencies": { "@trigger.dev/integrations": "workspace:*", "@trigger.dev/sdk": "workspace:*", - "jsx-slack": "^5.3.0", "zod": "^3.20.2" }, "devDependencies": { @@ -15,6 +14,6 @@ "tsx": "^3.12.0" }, "scripts": { - "dev": "tsx src/index.tsx" + "dev": "tsx src/index.ts" } } diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.ts new file mode 100644 index 00000000000..62ab9472a59 --- /dev/null +++ b/examples/send-to-slack/src/index.ts @@ -0,0 +1,39 @@ +import { Trigger, customEvent } from "@trigger.dev/sdk"; +import { slack } from "@trigger.dev/integrations"; +import { z } from "zod"; + +new Trigger({ + id: "send-to-slack-on-new-domain", + name: "Send to Slack on new domain", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: customEvent({ + name: "domain.created", + schema: z.object({ + id: z.string(), + customerId: z.string(), + domain: z.string(), + }), + }), + run: async (event, ctx) => { + await ctx.logger.info( + "Received domain.created event, waiting for 1 minutes..." + ); + + const response = await slack.postMessage("send-to-slack", { + channelName: "test-integrations", + text: `New domain created: ${event.domain} by customer ${event.customerId} cc @Eric #general`, + }); + + await ctx.waitFor("initial-wait", { seconds: 5 }); + + const secondResponse = await slack.postMessage("send-to-slack-channel-id", { + channelId: response.channel, + text: `Sent using the channelId: ${response.channel}`, + }); + + return {}; + }, +}).listen(); + diff --git a/examples/send-to-slack/src/index.tsx b/examples/send-to-slack/src/index.tsx deleted file mode 100644 index 08dbcb62b7d..00000000000 --- a/examples/send-to-slack/src/index.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { Trigger, customEvent, scheduleEvent } from "@trigger.dev/sdk"; -import { slack } from "@trigger.dev/integrations"; -import JSXSlack, { - Actions, - Blocks, - Button, - Section, - Select, - Option, -} from "jsx-slack"; -import { z } from "zod"; - -new Trigger({ - id: "send-to-slack-on-new-domain", - name: "Send to Slack on new domain", - apiKey: "trigger_dev_zC25mKNn6c0q", - endpoint: "ws://localhost:8889/ws", - logLevel: "debug", - on: customEvent({ - name: "domain.created", - schema: z.object({ - id: z.string(), - customerId: z.string(), - domain: z.string(), - }), - }), - run: async (event, ctx) => { - const response = await slack.postMessage("send-to-slack", { - channelName: "test-integrations", - text: `New domain created: ${event.domain} by customer ${event.customerId} cc @Eric #general`, - blocks: [ - { - type: "section", - text: { - type: "mrkdwn", - text: "Hello, Assistant to the Regional Manager Dwight! *Michael Scott* wants to know where you'd like to take the Paper Company investors to dinner tonight.\n\n *Please select a restaurant:*", - }, - }, - { - type: "divider", - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "*Farmhouse Thai Cuisine*\n:star::star::star::star: 1528 reviews\n They do have some vegan options, like the roti and curry, plus they have a ton of salad stuff and noodles can be ordered without meat!! They have something for everyone here", - }, - accessory: { - type: "image", - image_url: - "https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg", - alt_text: "alt text for image", - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "*Kin Khao*\n:star::star::star::star: 1638 reviews\n The sticky rice also goes wonderfully with the caramelized pork belly, which is absolutely melt-in-your-mouth and so soft.", - }, - accessory: { - type: "image", - image_url: - "https://s3-media2.fl.yelpcdn.com/bphoto/korel-1YjNtFtJlMTaC26A/o.jpg", - alt_text: "alt text for image", - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "*Ler Ros*\n:star::star::star::star: 2082 reviews\n I would really recommend the Yum Koh Moo Yang - Spicy lime dressing and roasted quick marinated pork shoulder, basil leaves, chili & rice powder.", - }, - accessory: { - type: "image", - image_url: - "https://s3-media2.fl.yelpcdn.com/bphoto/DawwNigKJ2ckPeDeDM7jAg/o.jpg", - alt_text: "alt text for image", - }, - }, - { - type: "divider", - }, - { - type: "actions", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: "Farmhouse", - emoji: true, - }, - value: "click_me_123", - }, - { - type: "button", - text: { - type: "plain_text", - text: "Kin Khao", - emoji: true, - }, - value: "click_me_123", - url: "https://google.com", - }, - { - type: "button", - text: { - type: "plain_text", - text: "Ler Ros", - emoji: true, - }, - value: "click_me_123", - url: "https://google.com", - }, - ], - }, - ], - }); - - await ctx.waitFor("initial-wait", { seconds: 5 }); - - await slack.postMessage("arnie", { - username: "Arnie", - icon_url: - "https://www.themoviedb.org/t/p/w500/zEMhugsgXIpnQqO31GpAJYMUZZ1.jpg", - channelName: "test-integrations", - text: getRandomQuote(), - }); - - return {}; - }, -}).listen(); - -function getRandomQuote() { - const arnoldQuotes = [ - "I'll be back.", - "Strength does not come from winning. Your struggles develop your strengths. When you go through hardships and decide not to surrender, that is strength.", - "The mind is the limit. As long as the mind can envision the fact that you can do something, you can do it, as long as you really believe 100 percent.", - "Success is not the key to happiness. Happiness is the key to success. If you love what you are doing, you will be successful.", - "For me life is continuously being hungry. The meaning of life is not simply to exist, to survive, but to move ahead, to go up, to achieve, to conquer.", - "The best activities for your health are pumping and humping.", - "I have a love interest in every one of my films: a gun.", - "You can have results or excuses, but not both.", - ]; - - return arnoldQuotes[Math.floor(Math.random() * arnoldQuotes.length)]; -} - -const BLOCK_ID = "issue.action.block"; -const BLOCK_ID_RATING = "issue.rating.block"; - -//every minute see how your employees are doing, we don't recommend this frequency 😉 -new Trigger({ - id: "slack-interactivity", - name: "Testing Slack Interactivity", - apiKey: "trigger_dev_zC25mKNn6c0q", - endpoint: "ws://localhost:8889/ws", - logLevel: "debug", - on: scheduleEvent({ - rateOf: { - minutes: 1, - }, - }), - run: async (event, ctx) => { - await slack.postMessage("jsx-test", { - channelName: "test-integrations", - //text appears in Slack notifications on mobile/desktop - text: "How is your progress today?", - //import and use JSXSlack to make creating rich messages much easier - blocks: JSXSlack( - -
How is your progress today?
- - - - - -
- ), - }); - }, -}).listen(); - -const BLOCK_ID_2 = "release.action.block"; - -new Trigger({ - id: "slack-block-interaction", - name: "Slack Block Interaction", - apiKey: "trigger_dev_zC25mKNn6c0q", - endpoint: "ws://localhost:8889/ws", - logLevel: "debug", - on: slack.events.blockActionInteraction({ - blockId: BLOCK_ID, - actionId: ["status-blocked", "status-help", "rating"], - }), - run: async (event, ctx) => { - //create promises from all the actions - const promises = event.actions.map((action) => { - switch (action.action_id) { - case "status-blocked": { - //the user is blocked so add a 😢 emoji as a reaction - if (event.message) { - return slack.addReaction("React to message", { - name: "cry", - timestamp: event.message.ts, - channelId: event.channel.id, - }); - } - } - case "status-help": { - //the user needs help so add an 🆘 emoji as a reaction - if (event.message) { - return slack.addReaction("React to message", { - name: "sos", - timestamp: event.message.ts, - channelId: event.channel.id, - }); - } - } - case "rating": { - if (action.type != "static_select") { - throw new Error("This action should be a select"); - } - - //post the rating as a message that appears below the original, - //only the user pressing the button will see this message - return slack.postMessageResponse( - "Added a comment to the issue", - event.response_url, - { - text: `You rated your day ${action.selected_option?.value} stars`, - replace_original: false, - } - ); - } - default: - return Promise.resolve(); - } - }); - - await Promise.all(promises); - }, -}).listen(); - -new Trigger({ - id: "slack-block-interaction-2", - name: "Slack Block Interaction 2", - apiKey: "trigger_dev_zC25mKNn6c0q", - endpoint: "ws://localhost:8889/ws", - logLevel: "debug", - on: slack.events.blockActionInteraction({ - blockId: BLOCK_ID_2, - }), - run: async (event, ctx) => { - if (!event.message) { - ctx.logger.debug(`No message found`); - return; - } - - await slack.addReaction("React to message", { - name: "thumbsup", - timestamp: event.message.ts, - channelId: event.channel.id, - }); - }, -}).listen(); diff --git a/examples/send-to-slack/tsconfig.json b/examples/send-to-slack/tsconfig.json index c441b3366ea..00ad22f1a85 100644 --- a/examples/send-to-slack/tsconfig.json +++ b/examples/send-to-slack/tsconfig.json @@ -1,9 +1,5 @@ { "extends": "@trigger.dev/tsconfig/examples.json", - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "**/*.test.*"], - "compilerOptions": { - "jsx": "react-jsx", // or "react-jsxdev" for development - "jsxImportSource": "jsx-slack" - } + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "**/*.test.*"] } diff --git a/examples/smoke-test/src/index.ts b/examples/smoke-test/src/index.ts index c082e487164..62093a8b1cf 100644 --- a/examples/smoke-test/src/index.ts +++ b/examples/smoke-test/src/index.ts @@ -23,13 +23,11 @@ const trigger = new Trigger({ await ctx.sendEvent("start-fire", { name: "smoke.test", payload: { baz: "banana" }, - delay: { until: new Date(Date.now() + 1000 * 60) }, }); await sendEvent("start-fire-2", { name: "smoke.test2", payload: { baz: "banana2" }, - delay: { minutes: 1 }, }); return { foo: "bar" }; @@ -38,24 +36,6 @@ const trigger = new Trigger({ trigger.listen(); -new Trigger({ - id: "smoke-test", - name: "Smoke Test", - apiKey: "trigger_dev_zC25mKNn6c0q", - endpoint: "ws://localhost:8889/ws", - logLevel: "debug", - on: customEvent({ - name: "smoke.test", - schema: z.object({ baz: z.string() }), - }), - run: async (event, ctx) => { - await ctx.logger.info("Inside the smoke test workflow, received event", { - event, - myDate: new Date(), - }); - }, -}).listen(); - new Trigger({ id: "log-tests", name: "My logs", diff --git a/flightcontrol.json b/flightcontrol.json index b2f3d87451a..c9d599df4ce 100644 --- a/flightcontrol.json +++ b/flightcontrol.json @@ -107,9 +107,6 @@ "fromParameterStore": "/Prod/webapp/POSTHOG_PROJECT_KEY" }, "TRIGGER_LOG_LEVEL": "debug", - "INTERNAL_TRIGGER_API_KEY": { - "fromParameterStore": "/Prod/webapp/INTERNAL_TRIGGER_API_KEY" - }, "PULSAR_ENABLED": "1", "PULSAR_DEBUG": true } @@ -150,7 +147,10 @@ "fromParameterStore": "/Prod/Pulsar/wss/Role" }, "PLATFORM_API_URL": "https://app.trigger.dev", - "TRIGGER_LOG_LEVEL": "debug" + "TRIGGER_LOG_LEVEL": "debug", + "TRIGGER_API_KEY": { + "fromParameterStore": "/Prod/webapp/TRIGGER_API_KEY" + } } }, { diff --git a/packages/common-schemas/src/events.ts b/packages/common-schemas/src/events.ts index b64e0d08948..5ece387271e 100644 --- a/packages/common-schemas/src/events.ts +++ b/packages/common-schemas/src/events.ts @@ -6,15 +6,6 @@ export const CustomEventSchema = z.object({ payload: JsonSchema, context: JsonSchema.optional(), timestamp: z.string().datetime().optional(), - delay: z - .union([ - z.object({ seconds: z.number().int() }), - z.object({ minutes: z.number().int() }), - z.object({ hours: z.number().int() }), - z.object({ days: z.number().int() }), - z.object({ until: z.string().datetime() }), - ]) - .optional(), }); export const SerializableCustomEventSchema = z.object({ @@ -22,15 +13,6 @@ export const SerializableCustomEventSchema = z.object({ payload: SerializableJsonSchema, context: SerializableJsonSchema.optional(), timestamp: z.string().datetime().optional(), - delay: z - .union([ - z.object({ seconds: z.number().int() }), - z.object({ minutes: z.number().int() }), - z.object({ hours: z.number().int() }), - z.object({ days: z.number().int() }), - z.object({ until: z.date() }), - ]) - .optional(), }); const EventMatcherSchema = z.union([ @@ -89,12 +71,3 @@ export const ManualWebhookSourceSchema = z.object({ }), event: z.string(), }); - -export const SlackInteractionSourceSchema = z.object({ - blockId: z.string(), - actionIds: z.array(z.string()), -}); - -export type SlackInteractionSource = z.infer< - typeof SlackInteractionSourceSchema ->; diff --git a/packages/common-schemas/src/triggers.ts b/packages/common-schemas/src/triggers.ts index 67fefdafc80..539adffa4fe 100644 --- a/packages/common-schemas/src/triggers.ts +++ b/packages/common-schemas/src/triggers.ts @@ -1,9 +1,5 @@ import { z } from "zod"; -import { - EventFilterSchema, - ScheduleSourceSchema, - SlackInteractionSourceSchema, -} from "./events"; +import { EventFilterSchema, ScheduleSourceSchema } from "./events"; import { JsonSchema } from "./json"; export const CustomEventTriggerSchema = z.object({ @@ -40,23 +36,11 @@ export const ScheduledEventTriggerSchema = z.object({ }); export type ScheduledEventTrigger = z.infer; -export const SlackInteractionTriggerSchema = z.object({ - type: z.literal("SLACK_INTERACTION"), - service: z.literal("slack"), - name: z.string(), - filter: EventFilterSchema, - source: SlackInteractionSourceSchema, -}); -export type SlackInteractionEventTrigger = z.infer< - typeof SlackInteractionTriggerSchema ->; - export const TriggerMetadataSchema = z.discriminatedUnion("type", [ CustomEventTriggerSchema, WebhookEventTriggerSchema, HttpEventTriggerSchema, ScheduledEventTriggerSchema, - SlackInteractionTriggerSchema, ]); export type TriggerMetadata = z.infer; diff --git a/packages/internal-integrations/src/services/index.ts b/packages/internal-integrations/src/services/index.ts index 512829b9465..a1b3baddafc 100644 --- a/packages/internal-integrations/src/services/index.ts +++ b/packages/internal-integrations/src/services/index.ts @@ -9,10 +9,7 @@ export type HttpServiceOptions = { baseUrl: string; }; -export type HttpResponse< - TResponseSchema extends z.ZodTypeAny, - TErrorResponseSchema extends z.ZodTypeAny = z.AnyZodObject -> = +export type HttpResponse = | { success: true; statusCode: number; @@ -23,7 +20,6 @@ export type HttpResponse< success: false; statusCode: number; headers: Record; - error?: z.infer; }; export class HttpService { @@ -36,19 +32,17 @@ export class HttpService { endpoint: HttpEndpoint, body?: z.infer ): Promise> { - const url = `${this.options.baseUrl}${endpoint.options.path}`; - const method = endpoint.options.method; - const headers = { - Authorization: `Bearer ${this.options.accessToken}`, - "Content-Type": "application/json; charset=utf-8", - }; - const actualBody = body ? JSON.stringify(body) : undefined; - - const response = await fetch(url, { - method, - headers, - body: actualBody, - }); + const response = await fetch( + `${this.options.baseUrl}${endpoint.options.path}`, + { + method: endpoint.options.method, + headers: { + Authorization: `Bearer ${this.options.accessToken}`, + "Content-Type": "application/json; charset=utf-8", + }, + body: body ? JSON.stringify(body) : undefined, + } + ); log( "%s%s response %d", @@ -83,13 +77,6 @@ export class HttpService { parsedJson, parsedJson.error ); - - return { - success: false, - statusCode: response.status, - headers: normalizeHeaders(response.headers), - error: { type: "zod-parse", issues: parsedJson.error.issues }, - }; } return { diff --git a/packages/internal-integrations/src/slack/index.ts b/packages/internal-integrations/src/slack/index.ts index 0f933af238c..94cd52d3374 100644 --- a/packages/internal-integrations/src/slack/index.ts +++ b/packages/internal-integrations/src/slack/index.ts @@ -1,3 +1,246 @@ -import { requests } from "./requests"; +import { HttpEndpoint, HttpService } from "../services"; +import { + DisplayProperties, + CacheService, + PerformedRequestResponse, + PerformRequestOptions, + RequestIntegration, + AccessInfo, +} from "../types"; +import { slack } from "@trigger.dev/providers"; +import debug from "debug"; +import { getAccessToken } from "../accessInfo"; +import { z } from "zod"; +import type { ReactNode } from "react"; -export { requests }; +const log = debug("trigger:integrations:slack"); + +const SendSlackMessageRequestBodySchema = + slack.schemas.PostMessageBodySchema.extend({ + link_names: z.literal(1), + }); + +class SlackRequestIntegration implements RequestIntegration { + #joinChannelEndpoint = new HttpEndpoint< + typeof slack.schemas.JoinConversationResponseSchema, + typeof slack.schemas.JoinConversationBodySchema + >({ + response: slack.schemas.JoinConversationResponseSchema, + method: "POST", + path: "/conversations.join", + }); + + #listConversationsEndpoint = new HttpEndpoint({ + response: slack.schemas.ListConversationsResponseSchema, + method: "GET", + path: "/conversations.list", + }); + + #postMessageEndpoint = new HttpEndpoint< + typeof slack.schemas.PostMessageResponseSchema, + typeof SendSlackMessageRequestBodySchema + >({ + response: slack.schemas.PostMessageResponseSchema, + method: "POST", + path: "/chat.postMessage", + }); + + constructor(private readonly baseUrl: string = "https://slack.com/api") {} + + perform(options: PerformRequestOptions): Promise { + switch (options.endpoint) { + case "chat.postMessage": { + return this.#postMessage( + options.accessInfo, + options.params, + options.cache + ); + } + default: { + throw new Error(`Unknown endpoint: ${options.endpoint}`); + } + } + } + + displayProperties(endpoint: string, params: any): DisplayProperties { + switch (endpoint) { + case "chat.postMessage": { + return { + title: `Post message to ${ + "channelName" in params ? params.channelName : params.channelId + }`, + properties: [ + { + key: "Text", + value: params.text, + }, + ], + }; + } + default: { + throw new Error(`Unknown endpoint: ${endpoint}`); + } + } + } + + renderComponent(input: any, output: any): ReactNode { + return null; + } + + async #postMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService + ): Promise { + const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params); + + log("chat.postMessage %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: this.baseUrl, + }); + + const channelId = await this.#findChannelId(service, params, cache); + + if (!channelId) { + return { + ok: false, + isRetryable: false, + response: { + output: { + message: `channelId not found`, + }, + context: { + statusCode: 404, + headers: {}, + }, + }, + }; + } + + log("found channelId %s", channelId); + + const response = await service.performRequest(this.#postMessageEndpoint, { + ...parsedParams, + link_names: 1, + channel: channelId, + }); + + if (!response.success) { + log("chat.postMessage failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: {}, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + if (!response.data.ok && response.data.error === "not_in_channel") { + log( + "chat.postMessage failed with not_in_channel, attempting to join channel %s", + channelId + ); + + // Attempt to join the channel, and then retry the request + const joinResponse = await service.performRequest( + this.#joinChannelEndpoint, + { + channel: channelId, + } + ); + + if (joinResponse.success && joinResponse.data.ok) { + log("joined channel %s, retrying postMessage", channelId); + + return this.#postMessage(accessInfo, params); + } + } + + const ok = response.data.ok; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + #isRetryable(statusCode: number): boolean { + return ( + statusCode === 408 || + statusCode === 429 || + statusCode === 500 || + statusCode === 502 || + statusCode === 503 || + statusCode === 504 + ); + } + + // Will use the conversations.list API (using fetch) to find the channel ID + // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ") + async #findChannelId( + service: HttpService, + params: z.infer, + cache?: CacheService + ): Promise { + if ("channelId" in params) { + return params.channelId; + } + + if (!("channelName" in params)) { + throw new Error("Invalid params, mising channelId and channelName"); + } + + //if the channelName starts with a #, remove it + if (params.channelName.startsWith("#")) { + params.channelName = params.channelName.substring(1); + } + + const cachedChannelId = await cache?.get(params.channelName); + + if (cachedChannelId) { + return cachedChannelId; + } + + const response = await service.performRequest( + this.#listConversationsEndpoint + ); + + if (response.success && response.data.ok) { + const { channels } = response.data; + + const channelInfo = channels.find( + (c: any) => c.name === params.channelName + ); + + if (channelInfo) { + await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24); + return channelInfo.id; + } + } + + return undefined; + } +} + +export const requests = new SlackRequestIntegration(); diff --git a/packages/internal-integrations/src/slack/requests.ts b/packages/internal-integrations/src/slack/requests.ts deleted file mode 100644 index 163bd77a6eb..00000000000 --- a/packages/internal-integrations/src/slack/requests.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { HttpEndpoint, HttpService } from "../services"; -import { - DisplayProperties, - CacheService, - PerformedRequestResponse, - PerformRequestOptions, - RequestIntegration, - AccessInfo, -} from "../types"; -import { slack } from "@trigger.dev/providers"; -import debug from "debug"; -import { getAccessToken } from "../accessInfo"; -import { z } from "zod"; -import type { ReactNode } from "react"; - -const log = debug("trigger:integrations:slack"); - -const SendSlackMessageRequestBodySchema = - slack.schemas.PostMessageBodySchema.extend({ - link_names: z.literal(1), - metadata: z - .object({ event_type: z.string(), event_payload: z.any() }) - .optional(), - }); - -class SlackRequestIntegration implements RequestIntegration { - #joinChannelEndpoint = new HttpEndpoint< - typeof slack.schemas.JoinConversationResponseSchema, - typeof slack.schemas.JoinConversationBodySchema - >({ - response: slack.schemas.JoinConversationResponseSchema, - method: "POST", - path: "/conversations.join", - }); - - #listConversationsEndpoint = new HttpEndpoint({ - response: slack.schemas.ListConversationsResponseSchema, - method: "GET", - path: "/conversations.list", - }); - - #postMessageEndpoint = new HttpEndpoint< - typeof slack.schemas.PostMessageResponseSchema, - typeof SendSlackMessageRequestBodySchema - >({ - response: slack.schemas.PostMessageResponseSchema, - method: "POST", - path: "/chat.postMessage", - }); - - #addReactionEndpoint = new HttpEndpoint< - typeof slack.schemas.AddReactionResponseSchema, - typeof slack.schemas.AddReactionOptionsSchema - >({ - response: slack.schemas.AddReactionResponseSchema, - method: "POST", - path: "/reactions.add", - }); - - constructor(private readonly baseUrl: string = "https://slack.com/api") {} - - perform(options: PerformRequestOptions): Promise { - switch (options.endpoint) { - case "chat.postMessage": { - return this.#postMessage( - options.accessInfo, - options.params, - options.cache, - options.metadata - ); - } - case "chat.postMessageResponse": { - return this.#postMessageResponse( - options.accessInfo, - options.params, - options.cache, - options.metadata - ); - } - case "reactions.add": { - return this.#addReaction( - options.accessInfo, - options.params, - options.cache, - options.metadata - ); - } - default: { - throw new Error(`Unknown endpoint: ${options.endpoint}`); - } - } - } - - displayProperties(endpoint: string, params: any): DisplayProperties { - switch (endpoint) { - case "chat.postMessage": { - return { - title: `Post message to ${ - "channelName" in params ? params.channelName : params.channelId - }`, - properties: [ - { - key: "Text", - value: params.text, - }, - ], - }; - } - case "chat.postMessageResponse": { - return { - title: `Post response`, - properties: [], - }; - } - case "reactions.add": { - return { - title: `Add reaction to message ${params.timestamp}`, - properties: [ - { - key: "Reaction", - value: params.name, - }, - ], - }; - } - - default: { - throw new Error(`Unknown endpoint: ${endpoint}`); - } - } - } - - renderComponent(input: any, output: any): ReactNode { - return null; - } - - async #postMessage( - accessInfo: AccessInfo, - params: any, - cache?: CacheService, - metadata?: Record - ): Promise { - const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params); - - log("chat.postMessage %O", parsedParams); - - const accessToken = getAccessToken(accessInfo); - - const service = new HttpService({ - accessToken, - baseUrl: this.baseUrl, - }); - - const channelId = await this.#findChannelId(service, params, cache); - - if (!channelId) { - return { - ok: false, - isRetryable: false, - response: { - output: { - message: `channelId not found`, - }, - context: { - statusCode: 404, - headers: {}, - }, - }, - }; - } - - log("found channelId %s", channelId); - - const response = await service.performRequest(this.#postMessageEndpoint, { - ...parsedParams, - link_names: 1, - channel: channelId, - metadata: metadata - ? { event_type: "post_message", event_payload: metadata } - : undefined, - }); - - if (!response.success) { - log("chat.postMessage failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.error, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - if (!response.data.ok && response.data.error === "not_in_channel") { - log( - "chat.postMessage failed with not_in_channel, attempting to join channel %s", - channelId - ); - - // Attempt to join the channel, and then retry the request - const joinResponse = await service.performRequest( - this.#joinChannelEndpoint, - { - channel: channelId, - } - ); - - if (joinResponse.success && joinResponse.data.ok) { - log("joined channel %s, retrying postMessage", channelId); - - return this.#postMessage(accessInfo, params); - } - } - - const ok = response.data.ok; - - const performedRequest = { - ok, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - async #addReaction( - accessInfo: AccessInfo, - params: any, - cache?: CacheService, - metadata?: Record - ): Promise { - const parsedParams = slack.schemas.AddReactionOptionsSchema.parse(params); - - log("reactions.add %O", parsedParams); - - const accessToken = getAccessToken(accessInfo); - - const service = new HttpService({ - accessToken, - baseUrl: this.baseUrl, - }); - - const channelId = await this.#findChannelId(service, parsedParams, cache); - - if (!channelId) { - return { - ok: false, - isRetryable: false, - response: { - output: { - message: `channelId not found`, - }, - context: { - statusCode: 404, - headers: {}, - }, - }, - }; - } - - log("found channelId %s", channelId); - - const response = await service.performRequest(this.#addReactionEndpoint, { - ...parsedParams, - // @ts-ignore - channel: channelId, - }); - - if (!response.success) { - log("reactions.add failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.error, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - if (!response.data.ok && response.data.error === "not_in_channel") { - log( - "reactions.add failed with not_in_channel, attempting to join channel %s", - channelId - ); - - // Attempt to join the channel, and then retry the request - const joinResponse = await service.performRequest( - this.#joinChannelEndpoint, - { - channel: channelId, - } - ); - - if (joinResponse.success && joinResponse.data.ok) { - log("joined channel %s, retrying reactions.add", channelId); - - return this.#addReaction(accessInfo, params); - } - } - - const ok = response.data.ok; - - const performedRequest = { - ok, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - async #postMessageResponse( - accessInfo: AccessInfo, - params: any, - cache?: CacheService, - metadata?: Record - ): Promise { - const parsedParams = z - .object({ - message: slack.schemas.PostMessageResponseOptionsSchema, - responseUrl: z.string(), - }) - .parse(params); - - log("chat.postMessageResponse %O", parsedParams); - - const response = await fetch(parsedParams.responseUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...parsedParams.message, - metadata: metadata - ? { event_type: "post_message_response", event_payload: metadata } - : undefined, - }), - }); - - if (!response.ok) { - log("chat.postMessageResponse failed %O", response); - - const error = await safeGetJson(response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.status), - response: { - output: error - ? error - : { name: `${response.status}`, message: response.statusText }, - context: { - statusCode: response.status, - headers: response.headers, - }, - }, - }; - } - - const output = await safeGetJson(response); - - const performedRequest = { - ok: response.ok, - isRetryable: this.#isRetryable(response.status), - response: { - output, - context: { - statusCode: response.status, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - #isRetryable(statusCode: number): boolean { - return ( - statusCode === 408 || - statusCode === 429 || - statusCode === 500 || - statusCode === 502 || - statusCode === 503 || - statusCode === 504 - ); - } - - // Will use the conversations.list API (using fetch) to find the channel ID - // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ") - async #findChannelId( - service: HttpService, - params: z.infer, - cache?: CacheService - ): Promise { - if ("channelId" in params) { - return params.channelId; - } - - if (!("channelName" in params)) { - throw new Error("Invalid params, mising channelId and channelName"); - } - - //if the channelName starts with a #, remove it - if (params.channelName.startsWith("#")) { - params.channelName = params.channelName.substring(1); - } - - const cachedChannelId = await cache?.get(params.channelName); - - if (cachedChannelId) { - return cachedChannelId; - } - - const response = await service.performRequest( - this.#listConversationsEndpoint - ); - - if (response.success && response.data.ok) { - const { channels } = response.data; - - const channelInfo = channels.find( - (c: any) => c.name === params.channelName - ); - - if (channelInfo) { - await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24); - return channelInfo.id; - } - } - - return undefined; - } -} - -export const requests = new SlackRequestIntegration(); - -function safeGetJson(response: Response): Promise { - return response.json().catch(() => null); -} diff --git a/packages/internal-integrations/src/types.ts b/packages/internal-integrations/src/types.ts index 68eba80be31..c9006a3159b 100644 --- a/packages/internal-integrations/src/types.ts +++ b/packages/internal-integrations/src/types.ts @@ -42,7 +42,6 @@ export type PerformRequestOptions = { endpoint: string; params: any; cache?: CacheService; - metadata?: Record; }; export type DisplayProperties = { diff --git a/packages/trigger-integrations/CHANGELOG.md b/packages/trigger-integrations/CHANGELOG.md index 37e8682b3f0..1ee237b061a 100644 --- a/packages/trigger-integrations/CHANGELOG.md +++ b/packages/trigger-integrations/CHANGELOG.md @@ -1,15 +1,5 @@ # @trigger.dev/integrations -## 0.1.15 - -### Patch Changes - -- b290410: Slack blocks support -- Updated dependencies [52d21ac] -- Updated dependencies [b290410] - - @trigger.dev/sdk@0.2.11 - - @trigger.dev/providers@0.1.9 - ## 0.1.14 ### Patch Changes diff --git a/packages/trigger-integrations/package.json b/packages/trigger-integrations/package.json index e323c395721..b3807920f08 100644 --- a/packages/trigger-integrations/package.json +++ b/packages/trigger-integrations/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/integrations", - "version": "0.1.15", + "version": "0.1.14", "description": "trigger.dev integrations", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-integrations/src/integrations/slack/events.ts b/packages/trigger-integrations/src/integrations/slack/events.ts deleted file mode 100644 index 67dc5afb914..00000000000 --- a/packages/trigger-integrations/src/integrations/slack/events.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { slack } from "@trigger.dev/providers"; -import { TriggerEvent } from "@trigger.dev/sdk"; - -export function blockActionInteraction(params: { - blockId: string; - actionId?: string | string[]; -}): TriggerEvent { - const actionIds = - typeof params.actionId === "undefined" - ? [] - : Array.isArray(params.actionId) - ? params.actionId - : [params.actionId]; - - return { - metadata: { - type: "SLACK_INTERACTION", - service: "slack", - name: "block.action", - filter: { - service: ["slack"], - payload: { - actions: { - block_id: [params.blockId], - action_id: actionIds, - }, - }, - event: ["block.action"], - }, - source: { - blockId: params.blockId, - actionIds, - }, - }, - schema: slack.schemas.blockAction, - }; -} diff --git a/packages/trigger-integrations/src/integrations/slack/index.ts b/packages/trigger-integrations/src/integrations/slack/index.ts index fe1fd2e2005..1e457edcc3e 100644 --- a/packages/trigger-integrations/src/integrations/slack/index.ts +++ b/packages/trigger-integrations/src/integrations/slack/index.ts @@ -1,9 +1,6 @@ import { getTriggerRun } from "@trigger.dev/sdk"; import { z } from "zod"; import { slack } from "@trigger.dev/providers"; -import * as events from "./events"; - -export { events }; export type PostMessageOptions = z.infer< typeof slack.schemas.PostMessageOptionsSchema @@ -34,64 +31,3 @@ export async function postMessage( return output; } - -export type PostMessageResponseOptions = z.infer< - typeof slack.schemas.PostMessageResponseOptionsSchema ->; - -export type PostMessageResponseResponse = z.infer< - typeof slack.schemas.PostMessageResponseSuccessResponseSchema ->; - -export async function postMessageResponse( - key: string, - responseUrl: string, - message: PostMessageResponseOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call postMessageResponse outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "slack", - endpoint: "chat.postMessageResponse", - params: { message, responseUrl }, - response: { - schema: slack.schemas.PostMessageResponseSuccessResponseSchema, - }, - }); - - return output; -} - -export type AddReactionOptions = z.infer< - typeof slack.schemas.AddReactionOptionsSchema ->; - -export type AddReactionResponse = z.infer< - typeof slack.schemas.AddReactionSuccessResponseSchema ->; - -export async function addReaction( - key: string, - options: AddReactionOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call addReaction outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "slack", - endpoint: "reactions.add", - params: options, - response: { - schema: slack.schemas.AddReactionSuccessResponseSchema, - }, - }); - - return output; -} diff --git a/packages/trigger-providers/CHANGELOG.md b/packages/trigger-providers/CHANGELOG.md index a021c9863b1..dba9c242ee6 100644 --- a/packages/trigger-providers/CHANGELOG.md +++ b/packages/trigger-providers/CHANGELOG.md @@ -1,11 +1,5 @@ # @trigger.dev/providers -## 0.1.9 - -### Patch Changes - -- b290410: Slack blocks support - ## 0.1.8 ### Patch Changes diff --git a/packages/trigger-providers/package.json b/packages/trigger-providers/package.json index bb8f5d49682..a3ebac3991f 100644 --- a/packages/trigger-providers/package.json +++ b/packages/trigger-providers/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/providers", - "version": "0.1.9", + "version": "0.1.8", "description": "trigger.dev API providers with schemas", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -23,7 +23,6 @@ "@types/react": "^18.0.26", "rimraf": "^3.0.2", "tsup": "^6.5.0", - "type-fest": "^3.5.3", "typescript": "^4.9.4" }, "scripts": { diff --git a/packages/trigger-providers/src/providers/slack/blocks.ts b/packages/trigger-providers/src/providers/slack/blocks.ts deleted file mode 100644 index 514b9725df9..00000000000 --- a/packages/trigger-providers/src/providers/slack/blocks.ts +++ /dev/null @@ -1,861 +0,0 @@ -import { z } from "zod"; - -export const imageElementSchema = z.object({ - type: z.literal("image"), - image_url: z.string(), - alt_text: z.string(), -}); - -export const plainTextElementSchema = z.object({ - type: z.literal("plain_text"), - text: z.string(), - emoji: z.boolean().optional(), -}); - -export const mrkdwnElementSchema = z.object({ - type: z.literal("mrkdwn"), - text: z.string(), - verbatim: z.boolean().optional(), -}); - -export const mrkdwnOptionSchema = z.object({ - text: mrkdwnElementSchema, - value: z.string().optional(), - url: z.string().optional(), - description: plainTextElementSchema.optional(), -}); - -export const plainTextOptionSchema = z.object({ - text: plainTextElementSchema, - value: z.string().optional(), - url: z.string().optional(), - description: plainTextElementSchema.optional(), -}); - -export const optionSchema = z.union([ - mrkdwnOptionSchema, - plainTextOptionSchema, -]); - -export const confirmSchema = z.object({ - title: plainTextElementSchema.optional(), - text: z.discriminatedUnion("type", [ - plainTextElementSchema, - mrkdwnElementSchema, - ]), - confirm: plainTextElementSchema.optional(), - deny: plainTextElementSchema.optional(), - style: z.union([z.literal("primary"), z.literal("danger")]).optional(), -}); - -/** - * @description Determines when an input element will return a - * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` interaction payload}. - */ -export const dispatchActionConfigSchema = z.object({ - /** - * @description An array of interaction types that you would like to receive a - * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload} for. Should be - * one or both of: - * `on_enter_pressed` — payload is dispatched when user presses the enter key while the input is in focus. Hint - * text will appear underneath the input explaining to the user to press enter to submit. - * `on_character_entered` — payload is dispatched when a character is entered (or removed) in the input. - */ - trigger_actions_on: z - .array( - z.union([ - z.literal("on_enter_pressed"), - z.literal("on_character_entered"), - ]) - ) - .optional(), -}); - -export const actionSchema = z.object({ - type: z.string(), - /** - * @description: An identifier for this action. You can use this when you receive an interaction payload to - * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique - * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. - */ - action_id: z.string().optional(), -}); - -export const actionIdSchema = z.object({ - /** - * @description: An identifier for this action. You can use this when you receive an interaction payload to - * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique - * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. - */ - action_id: z.string().optional(), -}); - -export const confirmableSchema = z.object({ - /** - * @description A {@see Confirm} object that defines an optional confirmation dialog after the element is interacted - * with. - */ - confirm: confirmSchema.optional(), -}); - -export const focusableSchema = z.object({ - /** - * @description Indicates whether the element will be set to auto focus within the - * {@link https://api.slack.com/reference/surfaces/views `view` object}. Only one element can be set to `true`. - * Defaults to `false`. - */ - focus_on_load: z.boolean().optional(), -}); - -export const placeholdableSchema = z.object({ - /** - * @description A {@see PlainTextElement} object that defines the placeholder text shown on the element. Maximum - * length for the `text` field in this object is 150 characters. - */ - placeholder: plainTextElementSchema.optional(), -}); - -export const dispatchableSchema = z.object({ - /** - * @description A {@see DispatchActionConfig} object that determines when during text input the element returns a - * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload}. - */ - dispatch_action_config: dispatchActionConfigSchema.optional(), -}); - -export const usersSelectSchema = z - .object({ - type: z.literal("users_select"), - initial_user: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiUsersSelectSchema = z - .object({ - type: z.literal("multi_users_select"), - initial_users: z.array(z.string()).optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const staticSelectSchema = z - .object({ - type: z.literal("static_select"), - initial_option: plainTextOptionSchema.optional(), - options: z.array(plainTextOptionSchema).optional(), - option_groups: z - .array( - z.object({ - label: plainTextElementSchema, - options: z.array(plainTextOptionSchema), - }) - ) - .optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiStaticSelectSchema = z - .object({ - type: z.literal("multi_static_select"), - initial_options: z.array(plainTextOptionSchema).optional(), - options: z.array(plainTextOptionSchema).optional(), - option_groups: z - .array( - z.object({ - label: plainTextElementSchema, - options: z.array(plainTextOptionSchema), - }) - ) - .optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const conversationsSelectSchema = z - .object({ - type: z.literal("conversations_select"), - initial_conversation: z.string().optional(), - response_url_enabled: z.boolean().optional(), - default_to_current_conversation: z.boolean().optional(), - filter: z - .object({ - include: z - .array( - z.union([ - z.literal("im"), - z.literal("mpim"), - z.literal("private"), - z.literal("public"), - ]) - ) - .optional(), - exclude_external_shared_channels: z.boolean().optional(), - exclude_bot_users: z.boolean().optional(), - }) - .optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiConversationsSelectSchema = z - .object({ - type: z.literal("multi_conversations_select"), - initial_conversations: z.array(z.string()).optional(), - max_selected_items: z.number().optional(), - default_to_current_conversation: z.boolean().optional(), - filter: z - .object({ - include: z - .array( - z.union([ - z.literal("im"), - z.literal("mpim"), - z.literal("private"), - z.literal("public"), - ]) - ) - .optional(), - exclude_external_shared_channels: z.boolean().optional(), - exclude_bot_users: z.boolean().optional(), - }) - .optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const channelsSelectSchema = z - .object({ - type: z.literal("channels_select"), - initial_channel: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiChannelsSelectSchema = z - .object({ - type: z.literal("multi_channels_select"), - initial_channels: z.array(z.string()).optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const externalSelectSchema = z - .object({ - type: z.literal("external_select"), - initial_option: plainTextOptionSchema.optional(), - min_query_length: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiExternalSelectSchema = z - .object({ - type: z.literal("multi_external_select"), - initial_options: z.array(plainTextOptionSchema).optional(), - min_query_length: z.number().optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const buttonSchema = z - .object({ - type: z.literal("button"), - text: plainTextElementSchema, - value: z.string().optional(), - url: z.string().optional(), - style: z.union([z.literal("danger"), z.literal("primary")]).optional(), - accessibility_label: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema); - -export const overflowSchema = z - .object({ - type: z.literal("overflow"), - options: z.array(plainTextOptionSchema), - }) - .merge(actionIdSchema) - .merge(confirmableSchema); - -export const datepickerSchema = z - .object({ - type: z.literal("datepicker"), - initial_date: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const timepickerSchema = z - .object({ - type: z.literal("timepicker"), - initial_time: z.string().optional(), - timezone: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const radioButtonsSchema = z - .object({ - type: z.literal("radio_buttons"), - initial_option: optionSchema.optional(), - options: z.array(optionSchema), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema); - -/** - * @description An element that allows the selection of a time of day formatted as a UNIX timestamp. On desktop - * clients, this time picker will take the form of a dropdown list merge the date picker will take the form of a dropdown - * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker merge date - * picker will use native UIs. - * {@link https://api.slack.com/reference/block-kit/block-elements#datetimepicker} - */ -export const dateTimepickerSchema = z - .object({ - type: z.literal("datetimepicker"), - /** - * @description The initial date merge time that is selected when the element is loaded, represented as a UNIX - * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date merge - * time August 10th, 2021 at 03:17pm PST. - */ - initial_date_time: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema); - -export const checkboxesSchema = z - .object({ - type: z.literal("checkboxes"), - initial_options: z.array(optionSchema).optional(), - options: z.array(optionSchema), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema); - -export const plainTextInputSchema = z - .object({ - type: z.literal("plain_text_input"), - initial_value: z.string().optional(), - multiline: z.boolean().optional(), - min_length: z.number().optional(), - max_length: z.number().optional(), - dispatch_action_config: dispatchActionConfigSchema.optional(), - focus_on_load: z.boolean().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -/** - * @description A URL input element, similar to the {@see PlainTextInput} element, creates a single line field where - * a user can enter URL-encoded data. - * {@link https://api.slack.com/reference/block-kit/block-elements#url} - */ -export const uRLInputSchema = z - .object({ - type: z.literal("url_text_input"), - /** - * @description The initial value in the URL input when it is loaded. - */ - initial_value: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -/** - * @description An email input element, similar to the {@see PlainTextInput} element, creates a single line field where - * a user can enter an email address. - * {@link https://api.slack.com/reference/block-kit/block-elements#email} - */ -export const emailInputSchema = z - .object({ - type: z.literal("email_text_input"), - /** - * @description The initial value in the email input when it is loaded. - */ - initial_value: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -/** - * @description A number input element, similar to the {@see PlainTextInput} element, creates a single line field where - * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, merge -10 are all - * valid input values. Decimal numbers are only allowed when `is_decimal_allowed` is equal to `true`. - * {@link https://api.slack.com/reference/block-kit/block-elements#number} - */ -export const numberInputSchema = z - .object({ - type: z.literal("number_input"), - /** - * @description Decimal numbers are allowed if this property is `true`, set the value to `false` otherwise. - */ - is_decimal_allowed: z.boolean(), - /** - * @description The initial value in the input when it is loaded. - */ - initial_value: z.string().optional(), - /** - * @description The minimum value, cannot be greater than `max_value`. - */ - min_value: z.string().optional(), - /** - * @description The maximum value, cannot be less than `min_value`. - */ - max_value: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const blockSchema = z.object({ - type: z.string(), - block_id: z.string().optional(), -}); - -export const imageBlockSchema = blockSchema.extend({ - type: z.literal("image"), - image_url: z.string(), - alt_text: z.string(), - title: plainTextElementSchema.optional(), -}); - -export const contextBlockSchema = blockSchema.extend({ - type: z.literal("context"), - elements: z.array( - z.discriminatedUnion("type", [ - imageElementSchema, - plainTextElementSchema, - mrkdwnElementSchema, - ]) - ), -}); - -export const dividerBlockSchema = blockSchema.extend({ - type: z.literal("divider"), -}); - -export const fileBlockSchema = blockSchema.extend({ - type: z.literal("file"), - source: z.string(), - external_id: z.string(), -}); - -export const headerBlockSchema = blockSchema.extend({ - type: z.literal("header"), - text: plainTextElementSchema, -}); - -export const messageMetadataEventPayloadObjectSchema = z.record( - z.union([z.string(), z.number(), z.boolean()]) -); - -export const messageAttachmentPreviewSchema = z.object({ - type: z.string().optional(), - can_remove: z.boolean().optional(), - title: plainTextElementSchema.optional(), - subtitle: plainTextElementSchema.optional(), - iconUrl: z.string().optional(), -}); - -export const optionFieldSchema = z.object({ - description: z.string().optional(), - text: z.string(), - value: z.string(), -}); - -export const confirmationSchema = z.object({ - dismiss_text: z.string().optional(), - ok_text: z.string().optional(), - text: z.string(), - title: z.string().optional(), -}); - -export const selectOptionSchema = z.object({ - label: z.string(), - value: z.string(), -}); - -export const callUserSlackSchema = z.object({ - slack_id: z.string(), -}); - -export const callUserExternalSchema = z.object({ - external_id: z.string(), - display_name: z.string(), - avatar_url: z.string(), -}); - -export const videoBlockSchema = blockSchema.extend({ - type: z.literal("video"), - video_url: z.string(), - thumbnail_url: z.string(), - alt_text: z.string(), - title: plainTextElementSchema, - title_url: z.string().optional(), - author_name: z.string().optional(), - provider_name: z.string().optional(), - provider_icon_url: z.string().optional(), - description: plainTextElementSchema.optional(), -}); - -export const dialogSchema = z.object({ - title: z.string(), - callback_id: z.string(), - elements: z.array( - z.object({ - type: z.union([ - z.literal("text"), - z.literal("textarea"), - z.literal("select"), - ]), - name: z.string(), - label: z.string(), - optional: z.boolean().optional(), - placeholder: z.string().optional(), - value: z.string().optional(), - max_length: z.number().optional(), - min_length: z.number().optional(), - hint: z.string().optional(), - subtype: z - .union([ - z.literal("email"), - z.literal("number"), - z.literal("tel"), - z.literal("url"), - ]) - .optional(), - data_source: z - .union([ - z.literal("users"), - z.literal("channels"), - z.literal("conversations"), - z.literal("external"), - ]) - .optional(), - selected_options: z.array(selectOptionSchema).optional(), - options: z.array(selectOptionSchema).optional(), - option_groups: z - .array( - z.object({ - label: z.string(), - options: z.array(selectOptionSchema), - }) - ) - .optional(), - min_query_length: z.number().optional(), - }) - ), - submit_label: z.string().optional(), - notify_on_cancel: z.boolean().optional(), - state: z.string().optional(), -}); - -const selectSchemas = [ - usersSelectSchema, - staticSelectSchema, - conversationsSelectSchema, - channelsSelectSchema, - externalSelectSchema, -]; - -export const selectSchema = z.discriminatedUnion("type", [ - usersSelectSchema, - staticSelectSchema, - conversationsSelectSchema, - channelsSelectSchema, - externalSelectSchema, -]); - -const multiSelectSchemas = [ - multiUsersSelectSchema, - multiStaticSelectSchema, - multiConversationsSelectSchema, - multiChannelsSelectSchema, - multiExternalSelectSchema, -]; -export const multiSelectSchema = z.discriminatedUnion("type", [ - multiUsersSelectSchema, - multiStaticSelectSchema, - multiConversationsSelectSchema, - multiChannelsSelectSchema, - multiExternalSelectSchema, -]); - -export const actionsBlockSchema = blockSchema.extend({ - type: z.literal("actions"), - elements: z.array( - z.discriminatedUnion("type", [ - buttonSchema, - overflowSchema, - datepickerSchema, - timepickerSchema, - dateTimepickerSchema, - ...selectSchemas, - radioButtonsSchema, - checkboxesSchema, - ]) - ), -}); - -export const sectionBlockSchema = blockSchema.extend({ - type: z.literal("section"), - text: z - .discriminatedUnion("type", [plainTextElementSchema, mrkdwnElementSchema]) - .optional(), - fields: z - .array( - z.discriminatedUnion("type", [ - plainTextElementSchema, - mrkdwnElementSchema, - ]) - ) - .optional(), - accessory: z - .discriminatedUnion("type", [ - buttonSchema, - overflowSchema, - datepickerSchema, - timepickerSchema, - ...selectSchemas, - ...multiSelectSchemas, - imageElementSchema, - radioButtonsSchema, - checkboxesSchema, - ]) - .optional(), -}); - -export const inputBlockSchema = blockSchema.extend({ - type: z.literal("input"), - label: plainTextElementSchema, - hint: plainTextElementSchema.optional(), - optional: z.boolean().optional(), - element: z.discriminatedUnion("type", [ - datepickerSchema, - ...selectSchemas, - ...multiSelectSchemas, - timepickerSchema, - dateTimepickerSchema, - plainTextInputSchema, - uRLInputSchema, - emailInputSchema, - numberInputSchema, - radioButtonsSchema, - checkboxesSchema, - ]), - dispatch_action: z.boolean().optional(), -}); - -export const messageMetadataSchema = z.object({ - event_type: z.string(), - event_payload: z.record( - z.union([ - z.string(), - z.number(), - z.boolean(), - messageMetadataEventPayloadObjectSchema, - z.array(messageMetadataEventPayloadObjectSchema), - ]) - ), -}); - -export const attachmentActionSchema = z.object({ - id: z.string().optional(), - confirm: confirmationSchema.optional(), - data_source: z - .union([ - z.literal("static"), - z.literal("channels"), - z.literal("conversations"), - z.literal("users"), - z.literal("external"), - ]) - .optional(), - min_query_length: z.number().optional(), - name: z.string().optional(), - options: z.array(optionFieldSchema).optional(), - option_groups: z - .array( - z.object({ - text: z.string(), - options: z.array(optionFieldSchema), - }) - ) - .optional(), - selected_options: z.array(optionFieldSchema).optional(), - style: z - .union([z.literal("default"), z.literal("primary"), z.literal("danger")]) - .optional(), - text: z.string(), - type: z.union([z.literal("button"), z.literal("select")]), - value: z.string().optional(), - url: z.string().optional(), -}); - -export const callUserSchema = z.union([ - callUserSlackSchema, - callUserExternalSchema, -]); - -const knownBlocks = [ - imageBlockSchema, - contextBlockSchema, - actionsBlockSchema, - dividerBlockSchema, - sectionBlockSchema, - inputBlockSchema, - fileBlockSchema, - headerBlockSchema, - videoBlockSchema, -]; - -export const knownBlockSchema = z.discriminatedUnion("type", [ - imageBlockSchema, - contextBlockSchema, - actionsBlockSchema, - dividerBlockSchema, - sectionBlockSchema, - inputBlockSchema, - fileBlockSchema, - headerBlockSchema, - videoBlockSchema, -]); - -const anyBlockSchema = z.discriminatedUnion("type", [ - imageBlockSchema, - contextBlockSchema, - actionsBlockSchema, - dividerBlockSchema, - sectionBlockSchema, - inputBlockSchema, - fileBlockSchema, - headerBlockSchema, - videoBlockSchema, -]); - -export const messageAttachmentSchema = z.object({ - blocks: z.array(anyBlockSchema).optional(), - fallback: z.string().optional(), - color: z - .union([ - z.literal("good"), - z.literal("warning"), - z.literal("danger"), - z.string(), - ]) - .optional(), - pretext: z.string().optional(), - author_name: z.string().optional(), - author_link: z.string().optional(), - author_icon: z.string().optional(), - title: z.string().optional(), - title_link: z.string().optional(), - text: z.string().optional(), - fields: z - .array( - z.object({ - title: z.string(), - value: z.string(), - short: z.boolean().optional(), - }) - ) - .optional(), - image_url: z.string().optional(), - thumb_url: z.string().optional(), - footer: z.string().optional(), - footer_icon: z.string().optional(), - ts: z.string().optional(), - actions: z.array(attachmentActionSchema).optional(), - callback_id: z.string().optional(), - mrkdwn_in: z - .array( - z.union([z.literal("pretext"), z.literal("text"), z.literal("fields")]) - ) - .optional(), - app_unfurl_url: z.string().optional(), - is_app_unfurl: z.boolean().optional(), - app_id: z.string().optional(), - bot_id: z.string().optional(), - preview: messageAttachmentPreviewSchema.optional(), -}); - -export const linkUnfurlsSchema = z.record(messageAttachmentSchema); - -export const homeViewSchema = z.object({ - type: z.literal("home"), - blocks: z.array(anyBlockSchema), - private_metadata: z.string().optional(), - callback_id: z.string().optional(), - external_id: z.string().optional(), -}); - -export const modalViewSchema = z.object({ - type: z.literal("modal"), - title: plainTextElementSchema, - blocks: z.array(anyBlockSchema), - close: plainTextElementSchema.optional(), - submit: plainTextElementSchema.optional(), - private_metadata: z.string().optional(), - callback_id: z.string().optional(), - clear_on_close: z.boolean().optional(), - notify_on_close: z.boolean().optional(), - external_id: z.string().optional(), -}); - -export const workflowStepViewSchema = z.object({ - type: z.literal("workflow_step"), - blocks: z.array(anyBlockSchema), - private_metadata: z.string().optional(), - callback_id: z.string().optional(), - submit_disabled: z.boolean().optional(), - external_id: z.string().optional(), -}); - -export const viewSchema: z.ZodDiscriminatedUnion< - "type", - [typeof homeViewSchema, typeof modalViewSchema, typeof workflowStepViewSchema] -> = z.discriminatedUnion("type", [ - homeViewSchema, - modalViewSchema, - workflowStepViewSchema, -]); diff --git a/packages/trigger-providers/src/providers/slack/index.ts b/packages/trigger-providers/src/providers/slack/index.ts index 51121adba63..03461c695a3 100644 --- a/packages/trigger-providers/src/providers/slack/index.ts +++ b/packages/trigger-providers/src/providers/slack/index.ts @@ -15,8 +15,6 @@ export const slack = { "groups:write", "im:write", "mpim:write", - "chat:write.customize", - "reactions:write", ], }, schemas, diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts deleted file mode 100644 index 2e341d605e3..00000000000 --- a/packages/trigger-providers/src/providers/slack/interactivity.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { z } from "zod"; -import { - knownBlockSchema, - mrkdwnElementSchema, - optionFieldSchema, - plainTextElementSchema, - viewSchema, -} from "./blocks"; - -const textSchema = z.discriminatedUnion("type", [ - plainTextElementSchema, - mrkdwnElementSchema, -]); -const blockActionType = z.union([ - z.literal("block_actions"), - z.literal("interactive_message"), -]); - -const sourceType = z.literal("message"); - -const commonActionSchema = z.object({ - action_id: z.string(), - block_id: z.string(), - action_ts: z.string(), -}); - -const buttonAction = z.object({ - type: z.literal("button"), - text: textSchema.optional(), - value: z.string(), -}); - -const selectedOptionSchema = z.object({ - text: z.object({ - type: z.string(), - text: z.string(), - emoji: z.boolean().optional(), - }), - value: z.string(), -}); -const placeholderSchema = z.object({ - type: z.string(), - text: z.string(), - emoji: z.boolean(), -}); -const staticSelectAction = z.object({ - type: z.literal("static_select"), - selected_option: selectedOptionSchema.nullable(), - placeholder: placeholderSchema.optional(), -}); - -const userSelectAction = z.object({ - type: z.literal("users_select"), - selected_user: z.string().nullable(), - initial_user: z.string().optional(), -}); - -const conversationsSelectAction = z.object({ - type: z.literal("conversations_select"), - selected_conversation: z.string().nullable(), - initial_conversation: z.string().optional(), -}); -const channelSelectAction = z.object({ - type: z.literal("channels_select"), - selected_channel: z.string().nullable(), - initial_channel: z.string().optional(), -}); - -const datePickerAction = z.object({ - type: z.literal("datepicker"), - selected_date: z.string().nullable(), - initial_date: z.string().optional(), -}); - -const checkboxesAction = z.object({ - type: z.literal("checkboxes"), - selected_options: z.array(optionFieldSchema), -}); - -const radioButtonsSchema = z.object({ - type: z.literal("radio_buttons"), - selectedOption: optionFieldSchema, -}); - -const timePickerSchema = z.object({ - type: z.literal("timepicker"), - selected_time: z.string().nullable(), - initial_time: z.string().optional(), -}); - -const plainTextInputSchema = z.object({ - type: z.literal("plain_text_input"), - value: z.string().nullable(), - initial_value: z.string().optional(), -}); - -const multiUsersSelectSchema = z.object({ - type: z.literal("multi_users_select"), - selected_users: z.array(z.string()), - initial_users: z.array(z.string()).optional(), -}); - -const multiStaticSelectSchema = z.object({ - type: z.literal("multi_static_select"), - selected_options: z.array(selectedOptionSchema), - placeholder: placeholderSchema.optional(), -}); - -const possibleActionsSchema = z.discriminatedUnion("type", [ - buttonAction, - staticSelectAction, - userSelectAction, - conversationsSelectAction, - channelSelectAction, - datePickerAction, - checkboxesAction, - radioButtonsSchema, - timePickerSchema, - plainTextInputSchema, - multiUsersSelectSchema, - multiStaticSelectSchema, -]); - -const actionSchema = possibleActionsSchema.and(commonActionSchema); - -//state.values.issue.action.block.rating.selected_option -const stateSchema = z.object({ - values: z.record(z.record(possibleActionsSchema)), -}); - -const userSchema = z.object({ - id: z.string(), - username: z.string(), - name: z.string(), - team_id: z.string(), -}); - -const containerSchema = z.object({ - type: sourceType, - message_ts: z.string(), - channel_id: z.string(), - is_ephemeral: z.boolean(), -}); - -const teamSchema = z.object({ id: z.string(), domain: z.string() }); -const channelSchema = z.object({ id: z.string(), name: z.string() }); - -const viewActionDataSchema = z.object({ - id: z.string(), - team_id: z.string(), - state: z - .object({ - values: z.record(z.record(z.any())), - }) - .optional(), - hash: z.string(), - previous_view_id: z.string().optional(), - root_view_id: z.string().optional(), - app_id: z.string().optional(), - app_installed_team_id: z.string().optional(), - bot_id: z.string().optional(), -}); -const viewActionSchema: Zod.ZodIntersection< - typeof viewSchema, - typeof viewActionDataSchema -> = viewSchema.and(viewActionDataSchema); - -const messageActionSchema = z.object({ - bot_id: z.string(), - type: sourceType, - text: z.string().optional(), - user: z.string().optional(), - ts: z.string(), - app_id: z.string().optional(), - blocks: z.array(knownBlockSchema).optional(), - team: z.string().optional(), - metadata: z - .object({ - event_type: z.string(), - event_payload: z.object({ requestId: z.string() }), - }) - .optional(), -}); - -export const blockAction: Zod.ZodObject<{ - type: typeof blockActionType; - user: typeof userSchema; - api_app_id: Zod.ZodString; - container: typeof containerSchema; - trigger_id: z.ZodOptional; - team: typeof teamSchema; - enterprise: Zod.ZodAny; - is_enterprise_install: Zod.ZodBoolean; - channel: typeof channelSchema; - view: z.ZodOptional; - message: z.ZodOptional; - state: z.ZodOptional; - response_url: Zod.ZodString; - actions: Zod.ZodArray; -}> = z.object({ - type: blockActionType, - user: userSchema, - api_app_id: z.string(), - container: containerSchema, - trigger_id: z.string().optional(), - team: teamSchema, - enterprise: z.any(), - is_enterprise_install: z.boolean(), - channel: channelSchema, - view: viewActionSchema.optional(), - message: messageActionSchema.optional(), - state: stateSchema.optional(), - response_url: z.string(), - actions: z.array(actionSchema), -}); diff --git a/packages/trigger-providers/src/providers/slack/prodmanifest.json b/packages/trigger-providers/src/providers/slack/prodmanifest.json index 2f63a247e93..d67716a9202 100644 --- a/packages/trigger-providers/src/providers/slack/prodmanifest.json +++ b/packages/trigger-providers/src/providers/slack/prodmanifest.json @@ -18,12 +18,10 @@ "groups:read", "im:read", "mpim:read", - "chat:write", - "reactions:write" + "chat:write" ], "bot": [ - "chat:write", - "reactions:write" + "chat:write" ] } }, diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts index c1fe80518cd..0a2d2427f0c 100644 --- a/packages/trigger-providers/src/providers/slack/schemas.ts +++ b/packages/trigger-providers/src/providers/slack/schemas.ts @@ -1,8 +1,4 @@ import { z } from "zod"; -import { knownBlockSchema } from "./blocks"; -import { blockAction } from "./interactivity"; - -export { blockAction }; export const PostMessageSuccessResponseSchema = z.object({ ok: z.literal(true), @@ -10,7 +6,7 @@ export const PostMessageSuccessResponseSchema = z.object({ ts: z.string(), message: z.object({ text: z.string(), - user: z.string().optional(), + user: z.string(), bot_id: z.string(), attachments: z.array(z.unknown()).optional(), type: z.string(), @@ -32,10 +28,6 @@ export const PostMessageResponseSchema = z.discriminatedUnion("ok", [ export const PostMessageBodySchema = z.object({ channel: z.string(), text: z.string(), - blocks: z.array(knownBlockSchema).optional(), - username: z.string().optional(), - icon_emoji: z.string().optional(), - icon_url: z.string().optional(), }); export const ChannelNameOrIdSchema = z.union([ @@ -46,17 +38,6 @@ export const ChannelNameOrIdSchema = z.union([ export const PostMessageOptionsSchema = z .object({ text: z.string(), - blocks: z.array(knownBlockSchema).optional(), - username: z.string().optional(), - icon_emoji: z.string().optional(), - icon_url: z.string().optional(), - }) - .and(ChannelNameOrIdSchema); - -export const AddReactionOptionsSchema = z - .object({ - name: z.string(), - timestamp: z.string(), }) .and(ChannelNameOrIdSchema); @@ -90,30 +71,3 @@ export const ListConversationsResponseSchema = z.discriminatedUnion("ok", [ ListConversationsSuccessResponseSchema, ErrorResponseSchema, ]); - -export const PostMessageResponseOptionsSchema = z.object({ - text: z.string().optional(), - blocks: z.array(knownBlockSchema).optional(), - response_type: z.enum(["in_channel"]).optional(), - replace_original: z.boolean().optional(), - delete_original: z.boolean().optional(), - thread_ts: z.string().optional(), -}); - -export const PostMessageResponseSuccessResponseSchema = z.object({ - ok: z.literal(true), -}); - -export const PostMessageResponseResponseSchema = z.discriminatedUnion("ok", [ - PostMessageResponseSuccessResponseSchema, - ErrorResponseSchema, -]); - -export const AddReactionSuccessResponseSchema = z.object({ - ok: z.literal(true), -}); - -export const AddReactionResponseSchema = z.discriminatedUnion("ok", [ - AddReactionSuccessResponseSchema, - ErrorResponseSchema, -]); diff --git a/packages/trigger-providers/src/schemaUtils.ts b/packages/trigger-providers/src/schemaUtils.ts deleted file mode 100644 index 7d3ca62b851..00000000000 --- a/packages/trigger-providers/src/schemaUtils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { z } from "zod"; -import { CamelCasedPropertiesDeep, SnakeCasedPropertiesDeep } from "type-fest"; - -/** Converts a Zod Schema from snake_case to camelCase */ -export function snakeToCamel( - schema: z.ZodType -) { - return schema.transform( - (object) => - deepSnakeToCamel(object) as unknown as CamelCasedPropertiesDeep< - typeof object - > - ); -} - -/** Converts a Zod Schema from camelCase to snake_case */ -export function camelToSnake( - schema: z.ZodType -) { - return schema.transform( - (object) => - deepCamelToSnake(object) as unknown as SnakeCasedPropertiesDeep< - typeof object - > - ); -} - -function deepSnakeToCamel(o: any): T { - if (isObject(o)) { - const n = {}; - - Object.keys(o).forEach((k) => { - // @ts-ignore - n[keySnakeToCamel(k)] = deepSnakeToCamel(o[k]); - }); - - return n as T; - } else if (isArray(o)) { - return o.map((i: any) => { - return deepSnakeToCamel(i); - }); - } - - return o; -} - -function keySnakeToCamel(s: string): string { - return s.replace(/([_][a-z])/gi, ($1) => { - return $1.toUpperCase().replace("-", "").replace("_", ""); - }); -} - -function deepCamelToSnake(o: any): T { - if (isObject(o)) { - const n = {}; - - Object.keys(o).forEach((k) => { - // @ts-ignore - n[keyCamelToSnake(k)] = deepCamelToSnake(o[k]); - }); - - return n as T; - } else if (isArray(o)) { - return o.map((i: any) => { - return deepCamelToSnake(i); - }); - } - - return o; -} - -function keyCamelToSnake(s: string): string { - return s - .replace(/[\w]([A-Z])/g, function (m) { - return m[0] + "_" + m[1]; - }) - .toLowerCase(); -} - -function isArray(a: any): boolean { - return Array.isArray(a); -} - -function isObject(o: any): boolean { - return o === Object(o) && !isArray(o) && typeof o !== "function"; -} diff --git a/packages/trigger-sdk/CHANGELOG.md b/packages/trigger-sdk/CHANGELOG.md index 5d75a5f4ede..1b84f33cfdc 100644 --- a/packages/trigger-sdk/CHANGELOG.md +++ b/packages/trigger-sdk/CHANGELOG.md @@ -1,12 +1,5 @@ # @trigger.dev/sdk -## 0.2.11 - -### Patch Changes - -- 52d21ac: Added support for delaying delivery when sending custom events -- b290410: Slack blocks support - ## 0.2.10 ### Patch Changes diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index 2870b334e03..ad5074fbc28 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sdk", - "version": "0.2.11", + "version": "0.2.10", "description": "trigger.dev Node.JS SDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36d67f12f19..6d6dadfd395 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,33 @@ importers: devDependencies: mintlify: 2.0.16 + apps/internal-triggers: + specifiers: + '@trigger.dev/integrations': ^0.1.13 + '@trigger.dev/sdk': ^0.2.10 + '@trigger.dev/tsconfig': workspace:* + '@types/node': '16' + '@types/pg': ^8.6.6 + dotenv: ^16.0.3 + pg: ^8.8.0 + rimraf: ^3.0.2 + tsup: ^6.5.0 + typescript: ^4.9.4 + zod: ^3.20.2 + dependencies: + '@trigger.dev/integrations': 0.1.13 + '@trigger.dev/sdk': 0.2.10 + pg: 8.8.0 + zod: 3.20.2 + devDependencies: + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + '@types/pg': 8.6.6 + dotenv: 16.0.3 + rimraf: 3.0.2 + tsup: 6.5.0_typescript@4.9.4 + typescript: 4.9.4 + apps/webapp: specifiers: '@aws-sdk/client-s3': ^3.186.0 @@ -184,7 +211,7 @@ importers: '@aws-sdk/client-s3': 3.245.0 '@aws-sdk/s3-request-presigner': 3.245.0 '@cfworker/json-schema': 1.12.5 - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 @@ -211,7 +238,7 @@ importers: '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/providers': link:../../packages/trigger-providers '@trigger.dev/sdk': link:../../packages/trigger-sdk - '@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne + '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle bcryptjs: 2.4.3 classnames: 2.3.2 clsx: 1.2.1 @@ -508,13 +535,11 @@ importers: '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' - jsx-slack: ^5.3.0 tsx: ^3.12.0 zod: ^3.20.2 dependencies: '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk - jsx-slack: 5.3.0 zod: 3.20.2 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -750,7 +775,7 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@react-email/render': 0.0.3_react@18.2.0 + '@react-email/render': 0.0.3 '@trigger.dev/providers': link:../trigger-providers '@trigger.dev/sdk': link:../trigger-sdk zod: 3.20.2 @@ -769,7 +794,6 @@ importers: rimraf: ^3.0.2 tiny-invariant: ^1.2.0 tsup: ^6.5.0 - type-fest: ^3.5.3 typescript: ^4.9.4 zod: ^3.20.2 dependencies: @@ -782,7 +806,6 @@ importers: '@types/react': 18.0.26 rimraf: 3.0.2 tsup: 6.5.0_typescript@4.9.4 - type-fest: 3.5.3 typescript: 4.9.4 packages/trigger-sdk: @@ -3457,13 +3480,12 @@ packages: prettier: 2.8.2 dev: false - /@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde: + /@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu: resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==} peerDependencies: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.3.2 '@codemirror/state': 6.2.0 @@ -3483,7 +3505,7 @@ packages: /@codemirror/lang-javascript/6.1.2: resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==} dependencies: - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/state': 6.2.0 @@ -4681,6 +4703,16 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false + /@react-email/render/0.0.3: + resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} + engines: {node: '>=18.0.0'} + dependencies: + pretty: 2.0.0 + react-dom: 18.2.0 + transitivePeerDependencies: + - react + dev: false + /@react-email/render/0.0.3_react@18.2.0: resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} engines: {node: '>=18.0.0'} @@ -4796,7 +4828,7 @@ packages: eslint: 8.31.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy - eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq + eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny eslint-plugin-jest: 26.9.0_ohsifnwenhmxgcp7mend4dnv74 eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0 @@ -5157,11 +5189,6 @@ packages: engines: {node: '>=10'} dev: true - /@slack/types/2.8.0: - resolution: {integrity: sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==} - engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - dev: false - /@swc/core-darwin-arm64/1.3.26: resolution: {integrity: sha512-FWWflBfKRYrUJtko2xiedC5XCa31O75IZZqnTWuLpe9g3C5tnUuF3M8LSXZS/dn6wprome1MhtG9GMPkSYkhkg==} engines: {node: '>=10'} @@ -5383,6 +5410,48 @@ packages: engines: {node: '>= 6'} dev: true + /@trigger.dev/integrations/0.1.13: + resolution: {integrity: sha512-2CfQNGqdC82om8Zr+PAdZZWov5SPAAAuVGV39d/MTNFLHhJZmJx47nbc5nnT/ujPSkoRrvOolwl93rVeceYoYA==} + dependencies: + '@react-email/render': 0.0.3 + '@trigger.dev/providers': 0.1.7 + '@trigger.dev/sdk': 0.2.10 + zod: 3.20.2 + transitivePeerDependencies: + - bufferutil + - encoding + - react + - supports-color + - utf-8-validate + dev: false + + /@trigger.dev/providers/0.1.7: + resolution: {integrity: sha512-wy7bh/bOGNgYbpUOJz59BkFU/0e3U0p7/5rClYY52QgAXZ9Z6c4JfocCqmYRxh+/Lb18N/wUd+vo5V9YfoyeIQ==} + dependencies: + '@shopify/admin-graphql-api-utilities': 2.0.1 + tiny-invariant: 1.3.1 + zod: 3.20.2 + dev: false + + /@trigger.dev/sdk/0.2.10: + resolution: {integrity: sha512-768mZmXi2iLgUfRIKOlpU3q9wRF6anhSnKoToJnRRzyAX3o6Kw5GkBXyM2QVuB5rt6psgbD+M4RKDaTnLpn/wQ==} + dependencies: + debug: 4.3.4 + evt: 2.4.13 + node-fetch: 2.6.7 + slug: 6.1.0 + ulid: 2.3.0 + uuid: 9.0.0 + ws: 8.12.0 + zod: 3.20.2 + zod-error: 1.1.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /@tsconfig/node10/1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -5653,6 +5722,14 @@ packages: /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + /@types/pg/8.6.6: + resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + dependencies: + '@types/node': 18.11.18 + pg-protocol: 1.5.0 + pg-types: 2.2.0 + dev: true + /@types/prismjs/1.26.0: resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} dev: true @@ -5916,18 +5993,17 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e: + /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' - '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 @@ -5936,14 +6012,11 @@ packages: '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne: + /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: - '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' - '@codemirror/theme-one-dark': '>=6.0.0' '@codemirror/view': '>=6.0.0' - codemirror: '>=6.0.0' react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: @@ -5952,14 +6025,13 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e - codemirror: 6.0.1_@lezer+common@1.0.2 + '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom + codemirror: 6.0.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' - - '@codemirror/lint' - '@codemirror/search' dev: false @@ -6763,6 +6835,11 @@ packages: resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} engines: {node: '>=0.10'} + /buffer-writer/2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + dev: false + /buffer/5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -7270,18 +7347,16 @@ packages: engines: {node: '>=0.10.0'} dev: false - /codemirror/6.0.1_@lezer+common@1.0.2: + /codemirror/6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 - transitivePeerDependencies: - - '@lezer/common' dev: false /collection-visit/1.0.0: @@ -8617,7 +8692,7 @@ packages: debug: 4.3.4 enhanced-resolve: 5.12.0 eslint: 8.31.0 - eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq + eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny get-tsconfig: 4.3.0 globby: 13.1.3 is-core-module: 2.11.0 @@ -8627,7 +8702,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama: + /eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -8652,6 +8727,7 @@ packages: debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 + eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy transitivePeerDependencies: - supports-color dev: true @@ -8676,7 +8752,7 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-import/2.27.4_qdjeohovcytra7xto5vgmxssaq: + /eslint-plugin-import/2.27.4_2ac3tknkazjoq5fxmuugu665ny: resolution: {integrity: sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==} engines: {node: '>=4'} peerDependencies: @@ -8694,7 +8770,7 @@ packages: doctrine: 2.1.0 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama + eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -11188,13 +11264,6 @@ packages: object.assign: 4.1.4 dev: true - /jsx-slack/5.3.0: - resolution: {integrity: sha512-xPYP8zLO31AKYiyuDJ88ykJzV/GvZUK+ktM4xE6ESIzrCICSd8nhrW/yaSyX45U+oD5LNTDUnhJvQe0LFDi63g==} - engines: {node: '>=14'} - dependencies: - '@slack/types': 2.8.0 - dev: false - /junk/3.1.0: resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} engines: {node: '>=8'} @@ -13124,6 +13193,10 @@ packages: semver: 6.3.0 dev: true + /packet-reader/1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + dev: false + /pako/0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} dev: true @@ -13282,6 +13355,59 @@ packages: is-reference: 3.0.1 dev: true + /pg-connection-string/2.5.0: + resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} + dev: false + + /pg-int8/1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + /pg-pool/3.5.2_pg@8.8.0: + resolution: {integrity: sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.8.0 + dev: false + + /pg-protocol/1.5.0: + resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==} + + /pg-types/2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + /pg/8.8.0: + resolution: {integrity: sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.5.0 + pg-pool: 3.5.2_pg@8.8.0 + pg-protocol: 1.5.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + dev: false + + /pgpass/1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.1.0 + dev: false + /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -13435,6 +13561,24 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array/2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + /postgres-bytea/1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + /postgres-date/1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + /postgres-interval/1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + /posthog-js/1.39.4: resolution: {integrity: sha512-Elpf1gwyuObueXi89iH+9pP+WhpkiivP8Qwej4RzOLwSTa7Floaa4rgAw7rnCnX1PtRoJ3F0kqb6q9T+aZjRiA==} dependencies: @@ -13790,6 +13934,15 @@ packages: shallow-equal: 1.2.1 dev: false + /react-dom/18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + scheduler: 0.23.0 + dev: false + /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -14957,6 +15110,11 @@ packages: through: 2.3.8 dev: true + /split2/4.1.0: + resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} + engines: {node: '>= 10.x'} + dev: false + /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -15879,11 +16037,6 @@ packages: engines: {node: '>=14.16'} dev: true - /type-fest/3.5.3: - resolution: {integrity: sha512-V2+og4j/rWReWvaFrse3s9g2xvUv/K9Azm/xo6CjIuq7oeGqsoimC7+9/A3tfvNcbQf8RPSVj/HV81fB4DJrjA==} - engines: {node: '>=14.16'} - dev: true - /type-is/1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} From 95d594457ccdf85b518292a9cf547042df45b83c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 23:24:07 -0800 Subject: [PATCH 08/71] Revert "Revert "Merge pull request #24 from triggerdotdev/dev"" This reverts commit 416c46d4cf4444cc9523ed49f08bec2d244361b2. --- apps/docs/examples/examples.mdx | 4 +- apps/docs/examples/resend.mdx | 5 + apps/docs/functions/send-event.mdx | 181 +++- .../apis/slack/actions/post-message.mdx | 148 ++- apps/docs/mint.json | 31 +- apps/docs/reference/send-event.mdx | 97 ++ apps/docs/welcome.mdx | 10 +- .../images/triggers/slack-interaction.png | Bin 0 -> 46640 bytes .../app/components/CreateNewWorkflow.tsx | 27 +- .../webapp/app/components/UserProfileMenu.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 2 +- .../app/components/triggers/Trigger.tsx | 40 + .../app/components/triggers/TriggerIcons.tsx | 9 + .../app/components/triggers/triggerLabel.tsx | 2 + apps/webapp/app/env.server.ts | 1 + .../models/workflowListPresenter.server.ts | 56 +- apps/webapp/app/models/workflowRun.server.ts | 35 +- .../models/workflowRunListPresenter.server.ts | 8 + .../workflows/$workflowSlug/runs/$runId.tsx | 42 +- .../workflows/$workflowSlug/test.tsx | 8 +- apps/webapp/app/routes/api/v1/events.ts | 27 +- .../internal/webhooks/slack/interactivity.ts | 24 + .../$organizationSlug/test/$workflowSlug.ts | 24 +- apps/webapp/app/services/emailAuth.server.tsx | 19 +- .../app/services/events/dispatch.server.ts | 20 +- .../app/services/events/ingest.server.ts | 13 +- .../events/ingestCustomEvent.server.ts | 60 ++ apps/webapp/app/services/gitHubAuth.server.ts | 11 + .../app/services/messageBroker.server.ts | 45 + .../performIntegrationRequest.server.ts | 3 + .../slack/handleInteractivity.server.ts | 73 ++ .../workflows/registerWorkflow.server.ts | 62 +- apps/webapp/app/utils/formData.ts | 10 + apps/webapp/app/utils/json.ts | 7 + apps/webapp/app/utils/objects.ts | 14 + .../migration.sql | 2 + .../migration.sql | 33 + apps/webapp/prisma/schema.prisma | 39 + examples/send-to-slack/package.json | 3 +- examples/send-to-slack/src/index.ts | 39 - examples/send-to-slack/src/index.tsx | 281 ++++++ examples/send-to-slack/tsconfig.json | 8 +- examples/smoke-test/src/index.ts | 20 + flightcontrol.json | 8 +- packages/common-schemas/src/events.ts | 27 + packages/common-schemas/src/triggers.ts | 18 +- .../src/services/index.ts | 37 +- .../internal-integrations/src/slack/index.ts | 247 +---- .../src/slack/requests.ts | 467 ++++++++++ packages/internal-integrations/src/types.ts | 1 + packages/trigger-integrations/CHANGELOG.md | 10 + packages/trigger-integrations/package.json | 2 +- .../src/integrations/slack/events.ts | 37 + .../src/integrations/slack/index.ts | 64 ++ packages/trigger-providers/CHANGELOG.md | 6 + packages/trigger-providers/package.json | 3 +- .../src/providers/slack/blocks.ts | 861 ++++++++++++++++++ .../src/providers/slack/index.ts | 2 + .../src/providers/slack/interactivity.ts | 215 +++++ .../src/providers/slack/prodmanifest.json | 6 +- .../src/providers/slack/schemas.ts | 48 +- packages/trigger-providers/src/schemaUtils.ts | 86 ++ packages/trigger-sdk/CHANGELOG.md | 7 + packages/trigger-sdk/package.json | 2 +- pnpm-lock.yaml | 245 +---- 65 files changed, 3336 insertions(+), 608 deletions(-) create mode 100644 apps/docs/reference/send-event.mdx create mode 100644 apps/webapp/app/assets/images/triggers/slack-interaction.png create mode 100644 apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts create mode 100644 apps/webapp/app/services/events/ingestCustomEvent.server.ts create mode 100644 apps/webapp/app/services/slack/handleInteractivity.server.ts create mode 100644 apps/webapp/app/utils/formData.ts create mode 100644 apps/webapp/app/utils/json.ts create mode 100644 apps/webapp/app/utils/objects.ts create mode 100644 apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql create mode 100644 apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql delete mode 100644 examples/send-to-slack/src/index.ts create mode 100644 examples/send-to-slack/src/index.tsx create mode 100644 packages/internal-integrations/src/slack/requests.ts create mode 100644 packages/trigger-integrations/src/integrations/slack/events.ts create mode 100644 packages/trigger-providers/src/providers/slack/blocks.ts create mode 100644 packages/trigger-providers/src/providers/slack/interactivity.ts create mode 100644 packages/trigger-providers/src/schemaUtils.ts diff --git a/apps/docs/examples/examples.mdx b/apps/docs/examples/examples.mdx index c8196f5e109..e1addc36d8c 100644 --- a/apps/docs/examples/examples.mdx +++ b/apps/docs/examples/examples.mdx @@ -17,5 +17,7 @@ You can use these workflows in your product by following the instructions on eac Post to Slack when a GitHub issue is created or modified. - + + Create a welcome email drip campaign. +
@@ -84,22 +84,14 @@ export function CreateNewWorkflowNoWorkflows() { Easily authenticate with APIs using the supported integrations below. If there's an integration we don't yet support,{" "} - - {" "} + let us know + {" "} and we'll add it. -
{getProviders(false).map((provider) => ( Fetch - & - - Webhooks -
Join the community diff --git a/apps/webapp/app/components/UserProfileMenu.tsx b/apps/webapp/app/components/UserProfileMenu.tsx index 2e02f22d2aa..52e73011419 100644 --- a/apps/webapp/app/components/UserProfileMenu.tsx +++ b/apps/webapp/app/components/UserProfileMenu.tsx @@ -23,7 +23,7 @@ export function UserProfileMenu({ user }: { user: User }) { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - + {user.name ? (
  • ; case "HTTP_ENDPOINT": break; - case "SLACK_INTERACTION": - return ; default: break; } @@ -31,43 +28,6 @@ export function TriggerBody({ trigger }: { trigger: TriggerMetadata }) { const workflowNodeUppercaseClasses = "uppercase text-slate-400 tracking-wide"; -function SlackInteraction({ - trigger, -}: { - trigger: SlackInteractionEventTrigger; -}) { - return ( -
    -
    - - Name - - - {trigger.name} - -
    -
    - - Block - - - {trigger.source.blockId} - -
    -
    - - Action - - - {trigger.source.actionIds.join(", ")} - -
    -
    - ); -} - -// trigger.source.actionIds; - function Webhook({ webhook }: { webhook: WebhookEventTrigger }) { return ( <> diff --git a/apps/webapp/app/components/triggers/TriggerIcons.tsx b/apps/webapp/app/components/triggers/TriggerIcons.tsx index ea22a7ce871..044bbfa0d89 100644 --- a/apps/webapp/app/components/triggers/TriggerIcons.tsx +++ b/apps/webapp/app/components/triggers/TriggerIcons.tsx @@ -5,7 +5,6 @@ import CustomEvent from "../../assets/images/triggers/custom-event.png"; import HttpEndpoint from "../../assets/images/triggers/http-endpoint.png"; import Schedule from "../../assets/images/triggers/schedule.png"; import Webhook from "../../assets/images/triggers/webhook.png"; -import SlackInteraction from "../../assets/images/triggers/slack-interaction.png"; import { triggerLabel } from "./triggerLabel"; type TriggerType = Workflow["type"]; @@ -53,14 +52,6 @@ export function TriggerTypeIcon({ return ( {triggerLabel(type)} ); - case "SLACK_INTERACTION": - return ( - {triggerLabel(type)} - ); default: return null; } diff --git a/apps/webapp/app/components/triggers/triggerLabel.tsx b/apps/webapp/app/components/triggers/triggerLabel.tsx index 5558fcb046d..07f1bca99e3 100644 --- a/apps/webapp/app/components/triggers/triggerLabel.tsx +++ b/apps/webapp/app/components/triggers/triggerLabel.tsx @@ -12,8 +12,6 @@ export function triggerLabel(type: TriggerType) { return "HTTP endpoint"; case "SCHEDULE": return "Scheduled"; - case "SLACK_INTERACTION": - return "Slack interaction"; default: return type; } diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index dda42842f76..43d8bde6a3c 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,7 +40,6 @@ const EnvironmentSchema = z.object({ PULSAR_ISSUER_URL: z.string().optional(), PULSAR_AUDIENCE: z.string().optional(), PULSAR_DEBUG: z.string().optional(), - INTERNAL_TRIGGER_API_KEY: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/models/workflowListPresenter.server.ts b/apps/webapp/app/models/workflowListPresenter.server.ts index 956610654b7..11831225574 100644 --- a/apps/webapp/app/models/workflowListPresenter.server.ts +++ b/apps/webapp/app/models/workflowListPresenter.server.ts @@ -1,8 +1,5 @@ -import type { SchedulerSource, InternalSource } from ".prisma/client"; -import { - ScheduleSourceSchema, - SlackInteractionSourceSchema, -} from "@trigger.dev/common-schemas"; +import type { SchedulerSource } from ".prisma/client"; +import { ScheduleSourceSchema } from "@trigger.dev/common-schemas"; import cronstrue from "cronstrue"; import type { DisplayProperties } from "internal-integrations"; import { github } from "internal-integrations"; @@ -63,8 +60,7 @@ export class WorkflowListPresenter { trigger: triggerProperties( workflow, workflow.externalSource ?? undefined, - workflow.schedulerSources[0] ?? undefined, - workflow.internalSources[0] ?? undefined + workflow.schedulerSources[0] ?? undefined ), integrations: { source: workflow.service @@ -109,17 +105,6 @@ function getWorkflows( orderBy: { createdAt: "desc" }, take: 1, }, - internalSources: { - select: { - source: true, - type: true, - }, - where: { - environmentId, - }, - orderBy: { createdAt: "desc" }, - take: 1, - }, runs: { select: { finishedAt: true, @@ -139,8 +124,7 @@ function getWorkflows( function triggerProperties( workflow: Pick, externalSource?: Pick, - schedulerSource?: Pick, - internalSource?: Pick + schedulerSource?: Pick ): { type: Workflow["type"]; typeTitle: string; @@ -173,14 +157,7 @@ function triggerProperties( }; } case "SCHEDULE": { - if (!schedulerSource) { - return { - type: workflow.type, - typeTitle: "Schedule", - title: "Not configured", - }; - } - + invariant(schedulerSource, "Scheduler source is required for schedule"); const source = ScheduleSourceSchema.parse(schedulerSource.schedule); if ("rateOf" in source) { @@ -228,29 +205,6 @@ function triggerProperties( typeTitle: "Custom event", title: `on: ${workflow.eventNames.join(", ")}`, }; - case "SLACK_INTERACTION": { - if (!internalSource) { - return { - type: workflow.type, - typeTitle: "Slack interaction", - title: "on: Slack interaction", - }; - } - - const slackSource = SlackInteractionSourceSchema.parse( - internalSource.source - ); - - return { - type: workflow.type, - typeTitle: "Slack interaction", - title: `block_id = ${slackSource.blockId}`, - properties: - slackSource.actionIds.length > 0 - ? [{ key: "Action ID", value: slackSource.actionIds.join(", ") }] - : undefined, - }; - } default: { return { type: workflow.type, diff --git a/apps/webapp/app/models/workflowRun.server.ts b/apps/webapp/app/models/workflowRun.server.ts index 123c6c9fb7a..aaa6194939c 100644 --- a/apps/webapp/app/models/workflowRun.server.ts +++ b/apps/webapp/app/models/workflowRun.server.ts @@ -1,16 +1,14 @@ -import type { - WorkflowRun, - WorkflowRunStatus, - WorkflowRunStep, -} from ".prisma/client"; +import type { WorkflowRun, WorkflowRunStep } from ".prisma/client"; +import type { WorkflowRunStatus } from ".prisma/client"; import type { CustomEventSchema, ErrorSchema, LogMessageSchema, } from "@trigger.dev/common-schemas"; +import { ulid } from "ulid"; import type { z } from "zod"; import { prisma } from "~/db.server"; -import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server"; +import { IngestEvent } from "~/services/events/ingest.server"; import { createStepOnce } from "./workflowRunStep.server"; export type { WorkflowRun, WorkflowRunStep, WorkflowRunStatus }; @@ -162,12 +160,20 @@ export async function triggerEventInRun( return; } - const ingestService = new IngestCustomEvent(); - - await ingestService.call({ - apiKey: workflowRun.environment.apiKey, - event, - }); + const ingestService = new IngestEvent(); + + await ingestService.call( + { + id: ulid(), + name: event.name, + type: "CUSTOM_EVENT", + service: "trigger", + payload: event.payload, + context: event.context, + apiKey: workflowRun.environment.apiKey, + }, + workflowRun.environment.organization + ); await prisma.workflowRunStep.update({ where: { id: step.step.id }, @@ -222,18 +228,13 @@ async function findWorkflowRunScopedToApiKey(id: string, apiKey: string) { export async function getMostRecentWorkflowRun({ workflowSlug, - organizationSlug, }: { workflowSlug: string; - organizationSlug: string; }) { return prisma.workflowRun.findFirst({ where: { workflow: { slug: workflowSlug, - organization: { - slug: organizationSlug, - }, }, }, include: { diff --git a/apps/webapp/app/models/workflowRunListPresenter.server.ts b/apps/webapp/app/models/workflowRunListPresenter.server.ts index e9fa5e4e0d3..470c5b8aad7 100644 --- a/apps/webapp/app/models/workflowRunListPresenter.server.ts +++ b/apps/webapp/app/models/workflowRunListPresenter.server.ts @@ -54,14 +54,6 @@ export class WorkflowRunListPresenter { where: { workflow: { slug: workflowSlug, - organization: { - slug: organizationSlug, - users: { - some: { - id: userId, - }, - }, - }, }, environment: { slug: environmentSlug, diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx index 65b3c33ab9d..0b143e9165c 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx @@ -9,10 +9,10 @@ import { } from "@heroicons/react/24/outline"; import { ArrowPathRoundedSquareIcon, - ChatBubbleOvalLeftEllipsisIcon, CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, + ChatBubbleOvalLeftEllipsisIcon, } from "@heroicons/react/24/solid"; import { useFetcher } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/server-runtime"; @@ -623,40 +623,6 @@ function CustomEventStep({ event }: { event: StepType }) { {event.input.name} - {"delay" in event.input && event.input.delay && ( - <> - - Delay - - - {"seconds" in event.input.delay ? ( - <> - {event.input.delay.seconds}{" "} - {event.input.delay.seconds > 1 ? "seconds" : "second"} - - ) : "minutes" in event.input.delay ? ( - <> - {event.input.delay.minutes}{" "} - {event.input.delay.minutes > 1 ? "minutes" : "minute"} - - ) : "hours" in event.input.delay ? ( - <> - {event.input.delay.hours}{" "} - {event.input.delay.hours > 1 ? "hours" : "hour"} - - ) : "days" in event.input.delay ? ( - <> - {event.input.delay.days}{" "} - {event.input.delay.days > 1 ? "days" : "day"} - - ) : "until" in event.input.delay ? ( - <>Until {event.input.delay.until} - ) : ( - <> - )} - - - )} Payload {event.input.context && ( @@ -712,11 +678,7 @@ function IntegrationRequestStep({ {request.input && ( <> - + )}
  • diff --git a/apps/docs/examples/resend.mdx b/apps/docs/examples/resend.mdx index 8e3d744d306..36ce42ac255 100644 --- a/apps/docs/examples/resend.mdx +++ b/apps/docs/examples/resend.mdx @@ -4,6 +4,11 @@ sidebarTitle: "Resend" description: "Here are some example workflows you could create using the Resend integration." --- + + Resend is in private beta, if you would like access please contact us at{" "} + help@trigger.dev + + ## Welcome email drip campaign This workflow will get triggered and a Slack notification and welcom email will be sent straight away. diff --git a/apps/docs/functions/send-event.mdx b/apps/docs/functions/send-event.mdx index b709fcaaae7..9c11909a576 100644 --- a/apps/docs/functions/send-event.mdx +++ b/apps/docs/functions/send-event.mdx @@ -4,9 +4,12 @@ sidebarTitle: "Send event" description: "Send events to trigger your custom events workflows." --- -## How to send an event +## Send an event -You can send an event from anywhere in your code, including from inside another workflow. +You can easily send an event from anywhere, including from inside another workflow. Events don't have to come from the same server as your workflow and can be sent as HTTP requests from any language (see our cURL example below for the format). + + + ```ts import { sendEvent } from "@trigger.dev/sdk"; @@ -19,12 +22,180 @@ await sendEvent("start-fire-2", { }); ``` + + + +```bash +curl --request POST \ + --url https://app.trigger.dev/api/v1/events \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data '{ + "id": "", + "event": { + "name": "user.created", + "payload": { + "userId": "123456" + } + } + }' +``` + + + + If you are calling this from inside a workflow, ensure that the first parameter is unique inside your workflow. ## Sending events from other workflows -It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them. +It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them. The below example shows how a scheduled workflow delegates the work via sending a custom event: + + + +```ts check-scheduler.ts +new Trigger({ + id: "check-scheduler", + name: "Check Scheduler", + on: scheduleEvent({ rateOf: { minutes: 10 } }), + run: async (event, context) => { + await context.sendEvent("health.check trigger.dev", { + name: "health.check", + payload: { + url: "https://trigger.dev", + host: "trigger.dev", + }, + }); + + await context.sendEvent("health.check docs.trigger.dev", { + name: "health.check", + payload: { + url: "https://docs.trigger.dev", + host: "docs.trigger.dev", + }, + }); + + await context.sendEvent("health.check app.trigger.dev", { + name: "health.check", + payload: { + url: "https://app.trigger.dev/healthcheck", + host: "app.trigger.dev", + }, + }); + }, +}).listen(); +``` + +```ts health-check.ts +export const healthCheck = new Trigger({ + id: "health-check", + name: "Health Check", + on: customEvent({ + name: "health.check", + schema: z.object({ + url: z.string().url(), + host: z.string(), + }), + }), + run: async (event, context) => { + const response = await context.fetch("fetch site", event.url, { + method: "GET", + retry: { + enabled: false, + }, + }); + + if (response.ok) { + await context.logger.info(`${event.host} is up!`); + return; + } + + await slack.postMessage("Site is down", { + channelName: "health-checks", + text: `${event.host} is down: ${response.status}`, + }); + }, +}).listen(); +``` + + + +## Delaying event delivery -## Writing a workflow that is triggered by an event +Through both our Node.js SDK and our HTTP API, you can delay the delivery of an event. This is useful if you want to send an event to trigger a workflow, but you want to wait for a certain amount of time before the event is delivered. -You should read [the documentation on custom events](/triggers/custom-events) to learn how to write a workflow that is triggered by an event. +Using the health check example above, we can delay the delivery of the `health.check` event in various ways: + +```ts +new Trigger({ + id: "check-scheduler", + name: "Check Scheduler", + on: scheduleEvent({ rateOf: { minutes: 10 } }), + run: async (event, context) => { + await context.sendEvent("health.check trigger.dev", { + name: "health.check", + payload: { + url: "https://trigger.dev", + host: "trigger.dev", + }, + delay: { until: new Date(Date.now() + 1000 * 60 * 5) }, // Delay until a specific date + }); + + await context.sendEvent("health.check docs.trigger.dev", { + name: "health.check", + payload: { + url: "https://docs.trigger.dev", + host: "docs.trigger.dev", + }, + delay: { minutes: 5 }, // Delay for a specific amount of time + }); + + await context.sendEvent("health.check app.trigger.dev", { + name: "health.check", + payload: { + url: "https://app.trigger.dev/healthcheck", + host: "app.trigger.dev", + }, + delay: { seconds: 30 }, // Delay for a specific amount of time + }); + }, +}).listen(); +``` + + + When sending events in the context of a workflow run, adding a delay DOES NOT + add a delay to the workflow run, as sending a custom event is a + fire-and-forget action. + + +You can add delays via the HTTP API by adding a `delay` object to the event: + +```bash +curl --request POST \ + --url https://app.trigger.dev/api/v1/events \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data '{ + "id": "", + "event": { + "name": "user.created", + "payload": { + "userId": "123456" + }, + "delay": { + "seconds": 30 + } + } + }' +``` + +## Event deduplication + +When sending events, you can specify an `id` for the event. If you send an event with the same `id` multiple times, only the first event will be delivered. This is useful if you want to ensure that an event is only delivered once, even if you send it multiple times. + +```ts +sendEvent({ + id: "my-event-id", + name: "my-event", + payload: { foo: "bar" }, +}); +``` diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index e1e9585a583..e2689e35f05 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -28,6 +28,14 @@ Publish slack messages to a public or private channel in your Slack Workspace as The formatted text of the message to be published, formatted as [mrkdwn](https://api.slack.com/reference/surfaces/formatting#basics). + + You can use blocks to create a rich message with images, buttons, and more. + + You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#) to see it in action. + + Full full detail on block see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). + + @@ -69,6 +77,8 @@ Publish slack messages to a public or private channel in your Slack Workspace as ## Example Workflows +### Notify Slack on New Star + ```typescript Notify Slack on New Star @@ -89,11 +99,7 @@ new Trigger({ }).listen(); ``` - - -## Example Response - -```json +```json Example response { "ok": true, "ts": "1673618429.084699", @@ -107,3 +113,135 @@ new Trigger({ } } ``` + + + +### Blocks with interactivity + +You can add interaction and richer visual elements using Blocks. The easiest way to add them is using `jsx-slack` (which is a separate npm package that you can install). + +This example messages your team every minute (!) and asks them how they're doing. There are two buttons and a dropdown menu. + +In this examples there are two workflows: + +1. Messages your team every minute (!) and asks them how they're doing. +2. Receives the interaction from users. It adds reactions when the buttons are pressed and posts a message when a user selects from the dropdown menu. + + + +```typescript Blocks with interactivity +import { slack } from "@trigger.dev/integrations"; +import JSXSlack, { + Actions, + Blocks, + Button, + Section, + Select, + Option, +} from "jsx-slack"; +import { z } from "zod"; + +const BLOCK_ID = "issue.action.block"; + +//1. every minute see how your employees are doing, we don't recommend this frequency 😉 +new Trigger({ + id: "slack-interactivity", + name: "Testing Slack Interactivity", + on: scheduleEvent({ + rateOf: { + minutes: 1, + }, + }), + run: async (event, ctx) => { + await slack.postMessage("jsx-test", { + channelName: "test-integrations", + //text appears in Slack notifications on mobile/desktop + text: "How is your progress today?", + //import and use JSXSlack to make creating rich messages much easier + blocks: JSXSlack( + +
    How is your progress today?
    + + + + + +
    + ), + }); + }, +}).listen(); + +//2. this workflow listens for Slack interactions filtered by the block id and actions we used above +new Trigger({ + id: "slack-block-interaction", + name: "Slack Block Interaction", + on: slack.events.blockActionInteraction({ + blockId: BLOCK_ID, + actionId: ["status-blocked", "status-help", "rating"], + }), + run: async (event, ctx) => { + //create promises from all the actions + const promises = event.actions.map((action) => { + switch (action.action_id) { + case "status-blocked": { + //the user is blocked so add a 😢 emoji as a reaction + if (event.message) { + return slack.addReaction("React to message", { + name: "cry", + timestamp: event.message.ts, + channelId: event.channel.id, + }); + } + } + case "status-help": { + //the user needs help so add an 🆘 emoji as a reaction + if (event.message) { + return slack.addReaction("React to message", { + name: "sos", + timestamp: event.message.ts, + channelId: event.channel.id, + }); + } + } + case "rating": { + if (action.type != "static_select") { + throw new Error("This action should be a select"); + } + + //post the rating as a message that appears below the original, + //only the user pressing the button will see this message + return slack.postMessageResponse( + "Added a comment to the issue", + event.response_url, + { + text: `You rated your day ${action.selected_option?.value} stars`, + replace_original: false, + } + ); + } + default: + return Promise.resolve(); + } + }); + + await Promise.all(promises); + }, +}).listen(); +``` + +
    diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 41e403a370c..77f128cced6 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -3,7 +3,7 @@ "logo": { "dark": "/logo/light.png", "light": "/logo/light.png", - "href": "https://trigger.dev" + "href": "https://app.trigger.dev" }, "favicon": "/images/favicon.png", "colors": { @@ -20,6 +20,16 @@ "default": "dark", "isHidden": true }, + "topbarLinks": [ + { + "name": "Login", + "url": "https://app.trigger.dev" + }, + { + "name": "Sign up", + "url": "https://app.trigger.dev" + } + ], "topbarCtaButton": { "type": "github", "url": "https://github.com/triggerdotdev/trigger.dev" @@ -39,11 +49,7 @@ "navigation": [ { "group": "Getting Started", - "pages": [ - "welcome", - "getting-started", - "get-help" - ] + "pages": ["welcome", "getting-started", "get-help"] }, { "group": "Examples", @@ -71,15 +77,11 @@ "pages": [ { "group": "Slack", - "pages": [ - "integrations/apis/slack/actions/post-message" - ] + "pages": ["integrations/apis/slack/actions/post-message"] }, { "group": "Resend.com", - "pages": [ - "integrations/apis/resend/actions/send-email" - ] + "pages": ["integrations/apis/resend/actions/send-email"] } ] }, @@ -111,7 +113,8 @@ "reference/trigger", "reference/custom-event", "reference/webhook-event", - "reference/schedule-event" + "reference/schedule-event", + "reference/send-event" ] }, { @@ -137,4 +140,4 @@ "github": "https://github.com/triggerdotdev/trigger.dev", "discord": "https://discord.gg/nkqV9xBYWy" } -} \ No newline at end of file +} diff --git a/apps/docs/reference/send-event.mdx b/apps/docs/reference/send-event.mdx new file mode 100644 index 00000000000..06c931d9f0f --- /dev/null +++ b/apps/docs/reference/send-event.mdx @@ -0,0 +1,97 @@ +--- +title: "sendEvent" +sidebarTitle: "sendEvent" +description: "Send a custom event to Trigger, from inside or outside of a workflow." +--- + +## Usage + +```ts +import { Trigger, customEvent } from "@trigger.dev/sdk"; + +new Trigger({ + id: "usage", + name: "usage", + on: customEvent({ + name: "user.created", + schema: z.any(), + }), + run: async (event, ctx) => { + await ctx.sendEvent("Sending context user.created event", { + name: "user.created", + payload: { + id: "1234_abc", + }, + }); + }, +}).listen(); +``` + +You can also use `sendEvent` by importing it from `@trigger.dev/sdk`: + +```ts +import { sendEvent } from "@trigger.dev/sdk"; + +await sendEvent("Sending context user.created event", { + name: "user.created", + payload: { + id: "1234_abc", + }, +}); +``` + + + If calling `sendEvent` from outside of a workflow run, it will make an HTTP + API request to app.trigger.dev to send the event. Make sure you set the + `TRIGGER_API_KEY` environment variable if you will be using it this way. + + +## Parameters + + + A unique key for the event in the context of a workflow run. Please see the + [Keys and Resumability](/guides/resumability) guide for more info. + + + + + + An optional unique ID for the event. If not provided, one will be + generated automatically (using `clid`). Set this field to perform event + deduplication. + + + + The name of the event. This is the name you set when creating the + `customEvent` trigger. + + + + The payload of the event. + + + + An optional timestamp for the event. If not provided, one will be generated. Must be in ISO 8601 format (e.g. `new Date().toISOString()`) + + + + An optional context object for the event. This can be used to pass + additional information about the event, such as the user who triggered + it. Note that this is not currently exposed to the workflow (coming soon). + + + + An optional delay object for the event. This can be used to delay the + event by a certain amount of time. Use one of the following options: + + - `{ seconds: number }` - delay the event by a certain number of seconds + - `{ minutes: number }` - delay the event by a certain number of minutes + - `{ hours: number }` - delay the event by a certain number of hours + - `{ days: number }` - delay the event by a certain number of days + - `{ until: Date }` - delay the event until a certain date + + See the [Delaying Event Delivery](/functions/send-event#delaying-event-delivery) docs for more info. + + + + diff --git a/apps/docs/welcome.mdx b/apps/docs/welcome.mdx index 98c6b3043e9..c723b28780f 100644 --- a/apps/docs/welcome.mdx +++ b/apps/docs/welcome.mdx @@ -8,19 +8,21 @@ Trigger.dev is an open source platform that enables developers to create event-d Workflows live in your codebase so you can use your existing types, functions, version control and IDE. They are triggered by us but run on your server so your private data is never exposed. +Go to [trigger.dev](https://app.trigger.dev) and sign up or login to your account. + _A workflow in action:_ ![Test a run](/images/workflow-demo.gif) - +  Quickly get up and running with Trigger.dev by following our quick start diff --git a/apps/webapp/app/assets/images/triggers/slack-interaction.png b/apps/webapp/app/assets/images/triggers/slack-interaction.png new file mode 100644 index 0000000000000000000000000000000000000000..9ea87cfe05e4d936cf34aa6ad94e68aa8ac0d0fb GIT binary patch literal 46640 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%0A2!=6iGfa+DNUZoa@TcbcesG%D; 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 literal 0 HcmV?d00001 diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 92961e3930b..591f8337cea 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -62,7 +62,7 @@ export function CreateNewWorkflowNoWorkflows() { diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx index f17914345af..f2ba3ac848d 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx @@ -18,11 +18,15 @@ import { requireUserId } from "~/services/session.server"; export const loader = async ({ request, params }: LoaderArgs) => { await requireUserId(request); - const { workflowSlug } = params; + const { workflowSlug, organizationSlug } = params; invariant(workflowSlug, "workflowSlug is required"); + invariant(organizationSlug, "organizationSlug is required"); try { - const latestRun = await getMostRecentWorkflowRun({ workflowSlug }); + const latestRun = await getMostRecentWorkflowRun({ + workflowSlug, + organizationSlug, + }); return typedjson({ latestRun }); } catch (error: any) { console.error(error); diff --git a/apps/webapp/app/routes/api/v1/events.ts b/apps/webapp/app/routes/api/v1/events.ts index 062d6aaf949..7f1ac0c42ce 100644 --- a/apps/webapp/app/routes/api/v1/events.ts +++ b/apps/webapp/app/routes/api/v1/events.ts @@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime"; import { CustomEventSchema } from "@trigger.dev/common-schemas"; import { z } from "zod"; import { authenticateApiRequest } from "~/services/apiAuth.server"; -import { IngestEvent } from "~/services/events/ingest.server"; +import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server"; const EventBodySchema = z.object({ id: z.string(), @@ -32,20 +32,13 @@ export async function action({ request }: ActionArgs) { return json({ error: eventBody.error.message }, { status: 400 }); } - const service = new IngestEvent(); - - const result = await service.call( - { - id: eventBody.data.id, - name: eventBody.data.event.name, - type: "CUSTOM_EVENT", - service: "trigger", - payload: eventBody.data.event.payload, - context: eventBody.data.event.context, - apiKey: authenticatedEnv.apiKey, - }, - authenticatedEnv.organization - ); - - return json(result.data); + const service = new IngestCustomEvent(); + + await service.call({ + id: eventBody.data.id, + event: eventBody.data.event, + apiKey: authenticatedEnv.apiKey, + }); + + return { status: 200 }; } diff --git a/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts new file mode 100644 index 00000000000..f3c988ef7e2 --- /dev/null +++ b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts @@ -0,0 +1,24 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { HandleSlackInteractivity } from "~/services/slack/handleInteractivity.server"; + +export async function action({ request }: ActionArgs) { + const formData = await request.formData(); + + const payload = formData.get("payload"); + + if (typeof payload !== "string") { + return { status: 400 }; + } + + const parsedPayload = JSON.parse(payload); + + const service = new HandleSlackInteractivity(); + + try { + await service.call(parsedPayload); + } catch (error) { + console.error(error); + } + + return { status: 200 }; +} diff --git a/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts b/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts index 30cd55775e0..9ddc914c4ef 100644 --- a/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts +++ b/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts @@ -13,6 +13,7 @@ import { import { getWorkflowFromSlugs } from "~/models/workflow.server"; import { CreateWorkflowTestRun } from "~/services/runs/createTestRun.server"; import { requireUserId } from "~/services/session.server"; +import { safeJsonParse } from "~/utils/json"; const requestSchema = z.object({ eventName: z.string(), @@ -39,7 +40,15 @@ export const action = async ({ request, params }: ActionArgs) => { const body = Object.fromEntries(formData.entries()); const { eventName, payload, source } = requestSchema.parse(body); - const jsonPayload = JSON.parse(payload); + const jsonPayload = safeJsonParse(payload); + + if (!jsonPayload) { + return redirectWithErrorMessage( + redirectUriForSource(source, organizationSlug, workflowSlug), + request, + "Invalid JSON payload" + ); + } const workflow = await getWorkflowFromSlugs({ userId, @@ -98,6 +107,19 @@ export const action = async ({ request, params }: ActionArgs) => { } }; +function redirectUriForSource( + source: "rerun" | "test", + organizationSlug: string, + workflowSlug: string, + runId?: string +) { + if (source === "rerun") { + return `/orgs/${organizationSlug}/workflows/${workflowSlug}/runs/${runId}`; + } else { + return `/orgs/${organizationSlug}/workflows/${workflowSlug}/test`; + } +} + function errorMessageForSource(source: "rerun" | "test") { if (source === "rerun") { return "Unable to rerun this workflow. Please contact help@trigger.dev for assistance."; diff --git a/apps/webapp/app/services/emailAuth.server.tsx b/apps/webapp/app/services/emailAuth.server.tsx index 83785dff738..23af9558570 100644 --- a/apps/webapp/app/services/emailAuth.server.tsx +++ b/apps/webapp/app/services/emailAuth.server.tsx @@ -6,6 +6,7 @@ import { findOrCreateUser } from "~/models/user.server"; import { env } from "~/env.server"; import { createFirstOrganization } from "~/models/organization.server"; import { sendMagicLinkEmail } from "~/services/email.server"; +import { taskQueue } from "./messageBroker.server"; let secret = env.MAGIC_LINK_SECRET; if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable."); @@ -32,15 +33,19 @@ const emailStrategy = new EmailLinkStrategy( authenticationMethod: "MAGIC_LINK", }); - console.log( - `User ${user.id} logged in with magic link. ${ - isNewUser ? "New user." : "" - }` - ); - if (isNewUser) { await createFirstOrganization(user); - emailProvider.scheduleWelcomeEmail(user); + await emailProvider.scheduleWelcomeEmail(user); + + await taskQueue.publish("SEND_INTERNAL_EVENT", { + id: user.id, + name: "user.created", + payload: { + id: user.id, + source: "MAGIC_LINK", + admin: user.admin, + }, + }); } return { userId: user.id }; diff --git a/apps/webapp/app/services/events/dispatch.server.ts b/apps/webapp/app/services/events/dispatch.server.ts index 7aa3366a96a..f547d2efca6 100644 --- a/apps/webapp/app/services/events/dispatch.server.ts +++ b/apps/webapp/app/services/events/dispatch.server.ts @@ -127,14 +127,22 @@ class EventMatcher { } function patternMatches(payload: any, pattern: any): boolean { - for (const [key, value] of Object.entries(pattern)) { - if (Array.isArray(value)) { - if (!value.includes(payload[key])) { + for (const [patternKey, patternValue] of Object.entries(pattern)) { + const payloadValue = payload[patternKey]; + + if (Array.isArray(patternValue)) { + if (patternValue.length > 0 && !patternValue.includes(payloadValue)) { return false; } - } else if (typeof value === "object") { - if (!patternMatches(payload[key], value)) { - return false; + } else if (typeof patternValue === "object") { + if (Array.isArray(payloadValue)) { + if (!payloadValue.some((item) => patternMatches(item, patternValue))) { + return false; + } + } else { + if (!patternMatches(payloadValue, patternValue)) { + return false; + } } } } diff --git a/apps/webapp/app/services/events/ingest.server.ts b/apps/webapp/app/services/events/ingest.server.ts index b3ba11f1ccf..6ed3c602020 100644 --- a/apps/webapp/app/services/events/ingest.server.ts +++ b/apps/webapp/app/services/events/ingest.server.ts @@ -25,20 +25,29 @@ export class IngestEvent { this.#prismaClient = prismaClient; } - public async call(options: IngestEventOptions, organization: Organization) { + public async call(options: IngestEventOptions, organization?: Organization) { const environment = options.apiKey ? await findEnvironmentByApiKey(options.apiKey) : undefined; + if (!environment && !organization) { + return { + status: "error" as const, + error: "No organization or environment found", + }; + } + const id = options.id ?? ulid(); + const organizationId = organization?.id ?? environment?.organizationId; + // Create a new event in the database const event = await this.#prismaClient.triggerEvent.create({ data: { id, organization: { connect: { - id: organization.id, + id: organizationId, }, }, environment: environment diff --git a/apps/webapp/app/services/events/ingestCustomEvent.server.ts b/apps/webapp/app/services/events/ingestCustomEvent.server.ts new file mode 100644 index 00000000000..06673e7762c --- /dev/null +++ b/apps/webapp/app/services/events/ingestCustomEvent.server.ts @@ -0,0 +1,60 @@ +import type { CustomEventSchema } from "@trigger.dev/common-schemas"; +import type { PublishOptions } from "internal-platform"; +import { ulid } from "ulid"; +import type { z } from "zod"; +import { taskQueue } from "~/services/messageBroker.server"; +import { omit } from "~/utils/objects"; +import { IngestEvent } from "./ingest.server"; + +export type IngestCustomEventOptions = { + id?: string; + apiKey: string; + event: z.infer; + isTest?: boolean; +}; + +export class IngestCustomEvent { + public async call(options: IngestCustomEventOptions) { + if (options.event.delay) { + const deliveryOptions: PublishOptions = + "until" in options.event.delay + ? { deliverAt: new Date(options.event.delay.until).getTime() } + : "seconds" in options.event.delay + ? { deliverAfter: options.event.delay.seconds * 1000 } + : "minutes" in options.event.delay + ? { deliverAfter: options.event.delay.minutes * 60 * 1000 } + : "hours" in options.event.delay + ? { deliverAfter: options.event.delay.hours * 60 * 60 * 1000 } + : "days" in options.event.delay + ? { deliverAfter: options.event.delay.days * 60 * 60 * 24 * 1000 } + : { deliverAfter: 0 }; + + await taskQueue.publish( + "INGEST_DELAYED_EVENT", + { + id: options.id, + apiKey: options.apiKey, + event: omit(options.event, ["delay"]), + }, + {}, + deliveryOptions + ); + + return; + } + + const ingestService = new IngestEvent(); + + await ingestService.call({ + id: options.id ?? ulid(), + name: options.event.name, + type: "CUSTOM_EVENT", + service: "trigger", + payload: options.event.payload, + context: options.event.context, + timestamp: options.event.timestamp, + apiKey: options.apiKey, + isTest: options.isTest, + }); + } +} diff --git a/apps/webapp/app/services/gitHubAuth.server.ts b/apps/webapp/app/services/gitHubAuth.server.ts index a32f3af627e..7b5b1a5cc57 100644 --- a/apps/webapp/app/services/gitHubAuth.server.ts +++ b/apps/webapp/app/services/gitHubAuth.server.ts @@ -5,6 +5,7 @@ import { createFirstOrganization } from "~/models/organization.server"; import { findOrCreateUser } from "~/models/user.server"; import type { AuthUser } from "./authUser"; import { scheduleWelcomeEmail } from "./email.server"; +import { taskQueue } from "./messageBroker.server"; const gitHubStrategy = new GitHubStrategy( { @@ -31,6 +32,16 @@ const gitHubStrategy = new GitHubStrategy( if (isNewUser) { await createFirstOrganization(user); await scheduleWelcomeEmail(user); + + await taskQueue.publish("SEND_INTERNAL_EVENT", { + id: user.id, + name: "user.created", + payload: { + id: user.id, + source: "GITHUB", + admin: user.admin, + }, + }); } return { diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts index 2e504053017..1d4f04cc161 100644 --- a/apps/webapp/app/services/messageBroker.server.ts +++ b/apps/webapp/app/services/messageBroker.server.ts @@ -1,5 +1,6 @@ import type { FetchOutputSchema } from "@trigger.dev/common-schemas"; import { + CustomEventSchema, JsonSchema, ScheduledEventPayloadSchema, } from "@trigger.dev/common-schemas"; @@ -34,6 +35,7 @@ import { InitiateDelay } from "./delays/initiateDelay.server"; import { ResolveDelay } from "./delays/resolveDelay.server"; import { sendEmail } from "./email.server"; import { DispatchEvent } from "./events/dispatch.server"; +import { IngestCustomEvent } from "./events/ingestCustomEvent.server"; import { HandleNewServiceConnection } from "./externalServices/handleNewConnection.server"; import { RegisterExternalSource } from "./externalSources/registerExternalSource.server"; import { CreateFetchRequest } from "./fetches/createFetchRequest.server"; @@ -49,6 +51,7 @@ import { WorkflowRunDisconnected } from "./runs/runDisconnected.server"; import { WorkflowRunTriggerTimeout } from "./runs/runTriggerTimeout.server"; import { DeliverScheduledEvent } from "./scheduler/deliverScheduledEvent.server"; import { RegisterSchedulerSource } from "./scheduler/registerSchedulerSource.server"; +import { omit } from "~/utils/objects"; let pulsarClient: PulsarClient; let triggerPublisher: ZodPublisher; @@ -372,6 +375,14 @@ const taskQueueCatalog = { data: z.object({ id: z.string() }), properties: z.object({}), }, + INGEST_DELAYED_EVENT: { + data: z.object({ + id: z.string().optional(), + apiKey: z.string(), + event: CustomEventSchema.omit({ delay: true }), + }), + properties: z.object({}), + }, TRIGGER_WORKFLOW_RUN: { data: z.object({ id: z.string() }), properties: z.object({}), @@ -419,6 +430,10 @@ const taskQueueCatalog = { }), properties: z.object({}), }, + SEND_INTERNAL_EVENT: { + data: CustomEventSchema.extend({ id: z.string() }), + properties: z.object({}), + }, }; function createTaskQueue() { @@ -640,6 +655,17 @@ function createTaskQueue() { return true; }, + INGEST_DELAYED_EVENT: async (id, data, properties, attributes) => { + if (attributes.redeliveryCount >= 4) { + return true; + } + + const ingestService = new IngestCustomEvent(); + + await ingestService.call(data); + + return true; + }, TRIGGER_WORKFLOW_RUN: async (id, data, properties) => { const run = await findWorklowRunById(data.id); @@ -679,6 +705,25 @@ function createTaskQueue() { return service.call(data.externalSourceId, data.payload); }, + SEND_INTERNAL_EVENT: async (id, data, properties, attributes) => { + if (attributes.redeliveryCount >= 4) { + return true; + } + + if (!env.INTERNAL_TRIGGER_API_KEY) { + return true; + } + + const service = new IngestCustomEvent(); + + await service.call({ + id: data.id, + event: omit(data, ["id"]), + apiKey: env.INTERNAL_TRIGGER_API_KEY, + }); + + return true; + }, }, }); diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index 320042e1fd5..0c2275dfc9b 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -231,6 +231,7 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, + metadata: { requestId: integrationRequest.id }, }); } case "shopify": { @@ -239,6 +240,7 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, + metadata: { requestId: integrationRequest.id }, }); } case "resend": { @@ -247,6 +249,7 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, + metadata: { requestId: integrationRequest.id }, }); } default: { diff --git a/apps/webapp/app/services/slack/handleInteractivity.server.ts b/apps/webapp/app/services/slack/handleInteractivity.server.ts new file mode 100644 index 00000000000..1d3b55c38a2 --- /dev/null +++ b/apps/webapp/app/services/slack/handleInteractivity.server.ts @@ -0,0 +1,73 @@ +import { slack } from "@trigger.dev/providers"; +import { ulid } from "ulid"; +import { generateErrorMessage } from "zod-error"; +import type { PrismaClient } from "~/db.server"; +import { prisma } from "~/db.server"; +import { IngestEvent } from "../events/ingest.server"; + +export class HandleSlackInteractivity { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(payload: unknown) { + console.log("payload", JSON.stringify(payload, null, 2)); + + const parsedPayload = slack.schemas.blockAction.safeParse(payload); + + if (!parsedPayload.success) { + console.error( + "Invalid payload", + generateErrorMessage(parsedPayload.error.issues) + ); + + return; + } + + if (parsedPayload.data.type !== "block_actions") { + return; + } + + if (!parsedPayload.data.message) { + return; + } + + if (!parsedPayload.data.message.metadata) { + return; + } + + const { requestId } = parsedPayload.data.message.metadata.event_payload; + + const integrationRequest = + await this.#prismaClient.integrationRequest.findUnique({ + where: { id: requestId }, + include: { + run: { + include: { + environment: true, + workflow: true, + }, + }, + }, + }); + + if (!integrationRequest) { + return; + } + + const ingestService = new IngestEvent(); + + await ingestService.call({ + id: ulid(), + type: "SLACK_INTERACTION", + name: "block.action", + service: "slack", + payload: parsedPayload.data, + apiKey: integrationRequest.run.environment.apiKey, + }); + + return true; + } +} diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts index d4b027d629c..90da212f76b 100644 --- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts +++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts @@ -45,12 +45,23 @@ export class RegisterWorkflow { } if (validation.data.trigger.service !== "trigger") { - await this.#upsertExternalSource( + const source = await this.upsertSource( validation.data, organization, workflow, environment ); + + if (source && source.status === "READY") { + await this.#prismaClient.workflow.update({ + where: { + id: workflow.id, + }, + data: { + status: "READY", + }, + }); + } } await this.#upsertEventRule( @@ -99,6 +110,18 @@ export class RegisterWorkflow { payload: WorkflowMetadata, organization: Organization ) { + const existingWorkflow = await this.#prismaClient.workflow.findUnique({ + where: { + organizationId_slug: { + organizationId: organization.id, + slug, + }, + }, + select: { + id: true, + }, + }); + const workflow = await this.#prismaClient.workflow.upsert({ where: { organizationId_slug: { @@ -130,10 +153,20 @@ export class RegisterWorkflow { }, }); + if (!existingWorkflow) { + await taskQueue.publish("SEND_INTERNAL_EVENT", { + id: workflow.id, + name: "workflow.created", + payload: { + id: workflow.id, + }, + }); + } + return workflow; } - async #upsertExternalSource( + async upsertSource( payload: WorkflowMetadata, organization: Organization, workflow: Workflow, @@ -198,6 +231,31 @@ export class RegisterWorkflow { return schedulerSource; } + case "SLACK_INTERACTION": { + if (!payload.trigger.source) { + return; + } + + return this.#prismaClient.internalSource.upsert({ + where: { + workflowId_environmentId: { + workflowId: workflow.id, + environmentId: environment.id, + }, + }, + update: { + source: payload.trigger.source, + }, + create: { + organizationId: organization.id, + workflowId: workflow.id, + environmentId: environment.id, + source: payload.trigger.source, + status: "READY", + type: "SLACK", + }, + }); + } default: { return; } diff --git a/apps/webapp/app/utils/formData.ts b/apps/webapp/app/utils/formData.ts new file mode 100644 index 00000000000..8290365263a --- /dev/null +++ b/apps/webapp/app/utils/formData.ts @@ -0,0 +1,10 @@ +import type { z } from "zod"; + +export function formDataAsObject( + formData: FormData, + schema: z.ZodSchema +): TValues { + const object = Object.fromEntries(formData.entries()); + const parsed = schema.parse(object); + return parsed; +} diff --git a/apps/webapp/app/utils/json.ts b/apps/webapp/app/utils/json.ts new file mode 100644 index 00000000000..2da50949c96 --- /dev/null +++ b/apps/webapp/app/utils/json.ts @@ -0,0 +1,7 @@ +export function safeJsonParse(json: string): unknown { + try { + return JSON.parse(json); + } catch (e) { + return null; + } +} diff --git a/apps/webapp/app/utils/objects.ts b/apps/webapp/app/utils/objects.ts new file mode 100644 index 00000000000..337fba9cac6 --- /dev/null +++ b/apps/webapp/app/utils/objects.ts @@ -0,0 +1,14 @@ +export function omit, K extends keyof T>( + obj: T, + keys: K[] +): Omit { + const result: any = {}; + + for (const key of Object.keys(obj)) { + if (!keys.includes(key as K)) { + result[key] = obj[key]; + } + } + + return result; +} diff --git a/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql b/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql new file mode 100644 index 00000000000..e9b9eceef04 --- /dev/null +++ b/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "TriggerType" ADD VALUE 'SLACK_INTERACTION'; diff --git a/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql b/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql new file mode 100644 index 00000000000..2bbe8c1cba7 --- /dev/null +++ b/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql @@ -0,0 +1,33 @@ +-- CreateEnum +CREATE TYPE "InternalSourceType" AS ENUM ('SLACK'); + +-- CreateEnum +CREATE TYPE "InternalSourceStatus" AS ENUM ('CREATED', 'READY', 'CANCELLED'); + +-- CreateTable +CREATE TABLE "InternalSource" ( + "id" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "workflowId" TEXT NOT NULL, + "environmentId" TEXT NOT NULL, + "type" "InternalSourceType" NOT NULL, + "source" JSONB NOT NULL, + "status" "InternalSourceStatus" NOT NULL DEFAULT 'CREATED', + "readyAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "InternalSource_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "InternalSource_workflowId_environmentId_key" ON "InternalSource"("workflowId", "environmentId"); + +-- AddForeignKey +ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "Workflow"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "RuntimeEnvironment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma index a3b0319619e..ca286c50ced 100644 --- a/apps/webapp/prisma/schema.prisma +++ b/apps/webapp/prisma/schema.prisma @@ -52,6 +52,7 @@ model Organization { externalSources ExternalSource[] eventRules EventRule[] schedulerSources SchedulerSource[] + internalSources InternalSource[] } model APIConnection { @@ -107,6 +108,7 @@ model RuntimeEnvironment { runs WorkflowRun[] eventRules EventRule[] schedulerSources SchedulerSource[] + internalSources InternalSource[] @@unique([organizationId, slug]) } @@ -134,6 +136,7 @@ model Workflow { rules EventRule[] externalServices ExternalService[] schedulerSources SchedulerSource[] + internalSources InternalSource[] service String @default("trigger") eventNames String[] @@ -181,6 +184,7 @@ enum TriggerType { HTTP_ENDPOINT EVENT_BRIDGE HTTP_POLLING + SLACK_INTERACTION } enum WorkflowStatus { @@ -257,6 +261,41 @@ enum SchedulerSourceStatus { CANCELLED } +// We should eventually move SchedulerSource to this as it's more generic +model InternalSource { + id String @id @default(cuid()) + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String + + workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade, onUpdate: Cascade) + workflowId String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + type InternalSourceType + source Json + + status InternalSourceStatus @default(CREATED) + + readyAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([workflowId, environmentId]) +} + +enum InternalSourceType { + SLACK +} + +enum InternalSourceStatus { + CREATED + READY + CANCELLED +} + model ExternalService { id String @id @default(cuid()) slug String diff --git a/examples/send-to-slack/package.json b/examples/send-to-slack/package.json index 93f35229e02..1f261d4ee88 100644 --- a/examples/send-to-slack/package.json +++ b/examples/send-to-slack/package.json @@ -6,6 +6,7 @@ "dependencies": { "@trigger.dev/integrations": "workspace:*", "@trigger.dev/sdk": "workspace:*", + "jsx-slack": "^5.3.0", "zod": "^3.20.2" }, "devDependencies": { @@ -14,6 +15,6 @@ "tsx": "^3.12.0" }, "scripts": { - "dev": "tsx src/index.ts" + "dev": "tsx src/index.tsx" } } diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.ts deleted file mode 100644 index 62ab9472a59..00000000000 --- a/examples/send-to-slack/src/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { slack } from "@trigger.dev/integrations"; -import { z } from "zod"; - -new Trigger({ - id: "send-to-slack-on-new-domain", - name: "Send to Slack on new domain", - apiKey: "trigger_dev_zC25mKNn6c0q", - endpoint: "ws://localhost:8889/ws", - logLevel: "debug", - on: customEvent({ - name: "domain.created", - schema: z.object({ - id: z.string(), - customerId: z.string(), - domain: z.string(), - }), - }), - run: async (event, ctx) => { - await ctx.logger.info( - "Received domain.created event, waiting for 1 minutes..." - ); - - const response = await slack.postMessage("send-to-slack", { - channelName: "test-integrations", - text: `New domain created: ${event.domain} by customer ${event.customerId} cc @Eric #general`, - }); - - await ctx.waitFor("initial-wait", { seconds: 5 }); - - const secondResponse = await slack.postMessage("send-to-slack-channel-id", { - channelId: response.channel, - text: `Sent using the channelId: ${response.channel}`, - }); - - return {}; - }, -}).listen(); - diff --git a/examples/send-to-slack/src/index.tsx b/examples/send-to-slack/src/index.tsx new file mode 100644 index 00000000000..08dbcb62b7d --- /dev/null +++ b/examples/send-to-slack/src/index.tsx @@ -0,0 +1,281 @@ +import { Trigger, customEvent, scheduleEvent } from "@trigger.dev/sdk"; +import { slack } from "@trigger.dev/integrations"; +import JSXSlack, { + Actions, + Blocks, + Button, + Section, + Select, + Option, +} from "jsx-slack"; +import { z } from "zod"; + +new Trigger({ + id: "send-to-slack-on-new-domain", + name: "Send to Slack on new domain", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: customEvent({ + name: "domain.created", + schema: z.object({ + id: z.string(), + customerId: z.string(), + domain: z.string(), + }), + }), + run: async (event, ctx) => { + const response = await slack.postMessage("send-to-slack", { + channelName: "test-integrations", + text: `New domain created: ${event.domain} by customer ${event.customerId} cc @Eric #general`, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Hello, Assistant to the Regional Manager Dwight! *Michael Scott* wants to know where you'd like to take the Paper Company investors to dinner tonight.\n\n *Please select a restaurant:*", + }, + }, + { + type: "divider", + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "*Farmhouse Thai Cuisine*\n:star::star::star::star: 1528 reviews\n They do have some vegan options, like the roti and curry, plus they have a ton of salad stuff and noodles can be ordered without meat!! They have something for everyone here", + }, + accessory: { + type: "image", + image_url: + "https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg", + alt_text: "alt text for image", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "*Kin Khao*\n:star::star::star::star: 1638 reviews\n The sticky rice also goes wonderfully with the caramelized pork belly, which is absolutely melt-in-your-mouth and so soft.", + }, + accessory: { + type: "image", + image_url: + "https://s3-media2.fl.yelpcdn.com/bphoto/korel-1YjNtFtJlMTaC26A/o.jpg", + alt_text: "alt text for image", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "*Ler Ros*\n:star::star::star::star: 2082 reviews\n I would really recommend the Yum Koh Moo Yang - Spicy lime dressing and roasted quick marinated pork shoulder, basil leaves, chili & rice powder.", + }, + accessory: { + type: "image", + image_url: + "https://s3-media2.fl.yelpcdn.com/bphoto/DawwNigKJ2ckPeDeDM7jAg/o.jpg", + alt_text: "alt text for image", + }, + }, + { + type: "divider", + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Farmhouse", + emoji: true, + }, + value: "click_me_123", + }, + { + type: "button", + text: { + type: "plain_text", + text: "Kin Khao", + emoji: true, + }, + value: "click_me_123", + url: "https://google.com", + }, + { + type: "button", + text: { + type: "plain_text", + text: "Ler Ros", + emoji: true, + }, + value: "click_me_123", + url: "https://google.com", + }, + ], + }, + ], + }); + + await ctx.waitFor("initial-wait", { seconds: 5 }); + + await slack.postMessage("arnie", { + username: "Arnie", + icon_url: + "https://www.themoviedb.org/t/p/w500/zEMhugsgXIpnQqO31GpAJYMUZZ1.jpg", + channelName: "test-integrations", + text: getRandomQuote(), + }); + + return {}; + }, +}).listen(); + +function getRandomQuote() { + const arnoldQuotes = [ + "I'll be back.", + "Strength does not come from winning. Your struggles develop your strengths. When you go through hardships and decide not to surrender, that is strength.", + "The mind is the limit. As long as the mind can envision the fact that you can do something, you can do it, as long as you really believe 100 percent.", + "Success is not the key to happiness. Happiness is the key to success. If you love what you are doing, you will be successful.", + "For me life is continuously being hungry. The meaning of life is not simply to exist, to survive, but to move ahead, to go up, to achieve, to conquer.", + "The best activities for your health are pumping and humping.", + "I have a love interest in every one of my films: a gun.", + "You can have results or excuses, but not both.", + ]; + + return arnoldQuotes[Math.floor(Math.random() * arnoldQuotes.length)]; +} + +const BLOCK_ID = "issue.action.block"; +const BLOCK_ID_RATING = "issue.rating.block"; + +//every minute see how your employees are doing, we don't recommend this frequency 😉 +new Trigger({ + id: "slack-interactivity", + name: "Testing Slack Interactivity", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: scheduleEvent({ + rateOf: { + minutes: 1, + }, + }), + run: async (event, ctx) => { + await slack.postMessage("jsx-test", { + channelName: "test-integrations", + //text appears in Slack notifications on mobile/desktop + text: "How is your progress today?", + //import and use JSXSlack to make creating rich messages much easier + blocks: JSXSlack( + +
    How is your progress today?
    + + + + + +
    + ), + }); + }, +}).listen(); + +const BLOCK_ID_2 = "release.action.block"; + +new Trigger({ + id: "slack-block-interaction", + name: "Slack Block Interaction", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: slack.events.blockActionInteraction({ + blockId: BLOCK_ID, + actionId: ["status-blocked", "status-help", "rating"], + }), + run: async (event, ctx) => { + //create promises from all the actions + const promises = event.actions.map((action) => { + switch (action.action_id) { + case "status-blocked": { + //the user is blocked so add a 😢 emoji as a reaction + if (event.message) { + return slack.addReaction("React to message", { + name: "cry", + timestamp: event.message.ts, + channelId: event.channel.id, + }); + } + } + case "status-help": { + //the user needs help so add an 🆘 emoji as a reaction + if (event.message) { + return slack.addReaction("React to message", { + name: "sos", + timestamp: event.message.ts, + channelId: event.channel.id, + }); + } + } + case "rating": { + if (action.type != "static_select") { + throw new Error("This action should be a select"); + } + + //post the rating as a message that appears below the original, + //only the user pressing the button will see this message + return slack.postMessageResponse( + "Added a comment to the issue", + event.response_url, + { + text: `You rated your day ${action.selected_option?.value} stars`, + replace_original: false, + } + ); + } + default: + return Promise.resolve(); + } + }); + + await Promise.all(promises); + }, +}).listen(); + +new Trigger({ + id: "slack-block-interaction-2", + name: "Slack Block Interaction 2", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: slack.events.blockActionInteraction({ + blockId: BLOCK_ID_2, + }), + run: async (event, ctx) => { + if (!event.message) { + ctx.logger.debug(`No message found`); + return; + } + + await slack.addReaction("React to message", { + name: "thumbsup", + timestamp: event.message.ts, + channelId: event.channel.id, + }); + }, +}).listen(); diff --git a/examples/send-to-slack/tsconfig.json b/examples/send-to-slack/tsconfig.json index 00ad22f1a85..c441b3366ea 100644 --- a/examples/send-to-slack/tsconfig.json +++ b/examples/send-to-slack/tsconfig.json @@ -1,5 +1,9 @@ { "extends": "@trigger.dev/tsconfig/examples.json", - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "**/*.test.*"] + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "**/*.test.*"], + "compilerOptions": { + "jsx": "react-jsx", // or "react-jsxdev" for development + "jsxImportSource": "jsx-slack" + } } diff --git a/examples/smoke-test/src/index.ts b/examples/smoke-test/src/index.ts index 62093a8b1cf..c082e487164 100644 --- a/examples/smoke-test/src/index.ts +++ b/examples/smoke-test/src/index.ts @@ -23,11 +23,13 @@ const trigger = new Trigger({ await ctx.sendEvent("start-fire", { name: "smoke.test", payload: { baz: "banana" }, + delay: { until: new Date(Date.now() + 1000 * 60) }, }); await sendEvent("start-fire-2", { name: "smoke.test2", payload: { baz: "banana2" }, + delay: { minutes: 1 }, }); return { foo: "bar" }; @@ -36,6 +38,24 @@ const trigger = new Trigger({ trigger.listen(); +new Trigger({ + id: "smoke-test", + name: "Smoke Test", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: customEvent({ + name: "smoke.test", + schema: z.object({ baz: z.string() }), + }), + run: async (event, ctx) => { + await ctx.logger.info("Inside the smoke test workflow, received event", { + event, + myDate: new Date(), + }); + }, +}).listen(); + new Trigger({ id: "log-tests", name: "My logs", diff --git a/flightcontrol.json b/flightcontrol.json index c9d599df4ce..b2f3d87451a 100644 --- a/flightcontrol.json +++ b/flightcontrol.json @@ -107,6 +107,9 @@ "fromParameterStore": "/Prod/webapp/POSTHOG_PROJECT_KEY" }, "TRIGGER_LOG_LEVEL": "debug", + "INTERNAL_TRIGGER_API_KEY": { + "fromParameterStore": "/Prod/webapp/INTERNAL_TRIGGER_API_KEY" + }, "PULSAR_ENABLED": "1", "PULSAR_DEBUG": true } @@ -147,10 +150,7 @@ "fromParameterStore": "/Prod/Pulsar/wss/Role" }, "PLATFORM_API_URL": "https://app.trigger.dev", - "TRIGGER_LOG_LEVEL": "debug", - "TRIGGER_API_KEY": { - "fromParameterStore": "/Prod/webapp/TRIGGER_API_KEY" - } + "TRIGGER_LOG_LEVEL": "debug" } }, { diff --git a/packages/common-schemas/src/events.ts b/packages/common-schemas/src/events.ts index 5ece387271e..b64e0d08948 100644 --- a/packages/common-schemas/src/events.ts +++ b/packages/common-schemas/src/events.ts @@ -6,6 +6,15 @@ export const CustomEventSchema = z.object({ payload: JsonSchema, context: JsonSchema.optional(), timestamp: z.string().datetime().optional(), + delay: z + .union([ + z.object({ seconds: z.number().int() }), + z.object({ minutes: z.number().int() }), + z.object({ hours: z.number().int() }), + z.object({ days: z.number().int() }), + z.object({ until: z.string().datetime() }), + ]) + .optional(), }); export const SerializableCustomEventSchema = z.object({ @@ -13,6 +22,15 @@ export const SerializableCustomEventSchema = z.object({ payload: SerializableJsonSchema, context: SerializableJsonSchema.optional(), timestamp: z.string().datetime().optional(), + delay: z + .union([ + z.object({ seconds: z.number().int() }), + z.object({ minutes: z.number().int() }), + z.object({ hours: z.number().int() }), + z.object({ days: z.number().int() }), + z.object({ until: z.date() }), + ]) + .optional(), }); const EventMatcherSchema = z.union([ @@ -71,3 +89,12 @@ export const ManualWebhookSourceSchema = z.object({ }), event: z.string(), }); + +export const SlackInteractionSourceSchema = z.object({ + blockId: z.string(), + actionIds: z.array(z.string()), +}); + +export type SlackInteractionSource = z.infer< + typeof SlackInteractionSourceSchema +>; diff --git a/packages/common-schemas/src/triggers.ts b/packages/common-schemas/src/triggers.ts index 539adffa4fe..67fefdafc80 100644 --- a/packages/common-schemas/src/triggers.ts +++ b/packages/common-schemas/src/triggers.ts @@ -1,5 +1,9 @@ import { z } from "zod"; -import { EventFilterSchema, ScheduleSourceSchema } from "./events"; +import { + EventFilterSchema, + ScheduleSourceSchema, + SlackInteractionSourceSchema, +} from "./events"; import { JsonSchema } from "./json"; export const CustomEventTriggerSchema = z.object({ @@ -36,11 +40,23 @@ export const ScheduledEventTriggerSchema = z.object({ }); export type ScheduledEventTrigger = z.infer; +export const SlackInteractionTriggerSchema = z.object({ + type: z.literal("SLACK_INTERACTION"), + service: z.literal("slack"), + name: z.string(), + filter: EventFilterSchema, + source: SlackInteractionSourceSchema, +}); +export type SlackInteractionEventTrigger = z.infer< + typeof SlackInteractionTriggerSchema +>; + export const TriggerMetadataSchema = z.discriminatedUnion("type", [ CustomEventTriggerSchema, WebhookEventTriggerSchema, HttpEventTriggerSchema, ScheduledEventTriggerSchema, + SlackInteractionTriggerSchema, ]); export type TriggerMetadata = z.infer; diff --git a/packages/internal-integrations/src/services/index.ts b/packages/internal-integrations/src/services/index.ts index a1b3baddafc..512829b9465 100644 --- a/packages/internal-integrations/src/services/index.ts +++ b/packages/internal-integrations/src/services/index.ts @@ -9,7 +9,10 @@ export type HttpServiceOptions = { baseUrl: string; }; -export type HttpResponse = +export type HttpResponse< + TResponseSchema extends z.ZodTypeAny, + TErrorResponseSchema extends z.ZodTypeAny = z.AnyZodObject +> = | { success: true; statusCode: number; @@ -20,6 +23,7 @@ export type HttpResponse = success: false; statusCode: number; headers: Record; + error?: z.infer; }; export class HttpService { @@ -32,17 +36,19 @@ export class HttpService { endpoint: HttpEndpoint, body?: z.infer ): Promise> { - const response = await fetch( - `${this.options.baseUrl}${endpoint.options.path}`, - { - method: endpoint.options.method, - headers: { - Authorization: `Bearer ${this.options.accessToken}`, - "Content-Type": "application/json; charset=utf-8", - }, - body: body ? JSON.stringify(body) : undefined, - } - ); + const url = `${this.options.baseUrl}${endpoint.options.path}`; + const method = endpoint.options.method; + const headers = { + Authorization: `Bearer ${this.options.accessToken}`, + "Content-Type": "application/json; charset=utf-8", + }; + const actualBody = body ? JSON.stringify(body) : undefined; + + const response = await fetch(url, { + method, + headers, + body: actualBody, + }); log( "%s%s response %d", @@ -77,6 +83,13 @@ export class HttpService { parsedJson, parsedJson.error ); + + return { + success: false, + statusCode: response.status, + headers: normalizeHeaders(response.headers), + error: { type: "zod-parse", issues: parsedJson.error.issues }, + }; } return { diff --git a/packages/internal-integrations/src/slack/index.ts b/packages/internal-integrations/src/slack/index.ts index 94cd52d3374..0f933af238c 100644 --- a/packages/internal-integrations/src/slack/index.ts +++ b/packages/internal-integrations/src/slack/index.ts @@ -1,246 +1,3 @@ -import { HttpEndpoint, HttpService } from "../services"; -import { - DisplayProperties, - CacheService, - PerformedRequestResponse, - PerformRequestOptions, - RequestIntegration, - AccessInfo, -} from "../types"; -import { slack } from "@trigger.dev/providers"; -import debug from "debug"; -import { getAccessToken } from "../accessInfo"; -import { z } from "zod"; -import type { ReactNode } from "react"; +import { requests } from "./requests"; -const log = debug("trigger:integrations:slack"); - -const SendSlackMessageRequestBodySchema = - slack.schemas.PostMessageBodySchema.extend({ - link_names: z.literal(1), - }); - -class SlackRequestIntegration implements RequestIntegration { - #joinChannelEndpoint = new HttpEndpoint< - typeof slack.schemas.JoinConversationResponseSchema, - typeof slack.schemas.JoinConversationBodySchema - >({ - response: slack.schemas.JoinConversationResponseSchema, - method: "POST", - path: "/conversations.join", - }); - - #listConversationsEndpoint = new HttpEndpoint({ - response: slack.schemas.ListConversationsResponseSchema, - method: "GET", - path: "/conversations.list", - }); - - #postMessageEndpoint = new HttpEndpoint< - typeof slack.schemas.PostMessageResponseSchema, - typeof SendSlackMessageRequestBodySchema - >({ - response: slack.schemas.PostMessageResponseSchema, - method: "POST", - path: "/chat.postMessage", - }); - - constructor(private readonly baseUrl: string = "https://slack.com/api") {} - - perform(options: PerformRequestOptions): Promise { - switch (options.endpoint) { - case "chat.postMessage": { - return this.#postMessage( - options.accessInfo, - options.params, - options.cache - ); - } - default: { - throw new Error(`Unknown endpoint: ${options.endpoint}`); - } - } - } - - displayProperties(endpoint: string, params: any): DisplayProperties { - switch (endpoint) { - case "chat.postMessage": { - return { - title: `Post message to ${ - "channelName" in params ? params.channelName : params.channelId - }`, - properties: [ - { - key: "Text", - value: params.text, - }, - ], - }; - } - default: { - throw new Error(`Unknown endpoint: ${endpoint}`); - } - } - } - - renderComponent(input: any, output: any): ReactNode { - return null; - } - - async #postMessage( - accessInfo: AccessInfo, - params: any, - cache?: CacheService - ): Promise { - const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params); - - log("chat.postMessage %O", parsedParams); - - const accessToken = getAccessToken(accessInfo); - - const service = new HttpService({ - accessToken, - baseUrl: this.baseUrl, - }); - - const channelId = await this.#findChannelId(service, params, cache); - - if (!channelId) { - return { - ok: false, - isRetryable: false, - response: { - output: { - message: `channelId not found`, - }, - context: { - statusCode: 404, - headers: {}, - }, - }, - }; - } - - log("found channelId %s", channelId); - - const response = await service.performRequest(this.#postMessageEndpoint, { - ...parsedParams, - link_names: 1, - channel: channelId, - }); - - if (!response.success) { - log("chat.postMessage failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: {}, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - if (!response.data.ok && response.data.error === "not_in_channel") { - log( - "chat.postMessage failed with not_in_channel, attempting to join channel %s", - channelId - ); - - // Attempt to join the channel, and then retry the request - const joinResponse = await service.performRequest( - this.#joinChannelEndpoint, - { - channel: channelId, - } - ); - - if (joinResponse.success && joinResponse.data.ok) { - log("joined channel %s, retrying postMessage", channelId); - - return this.#postMessage(accessInfo, params); - } - } - - const ok = response.data.ok; - - const performedRequest = { - ok, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - #isRetryable(statusCode: number): boolean { - return ( - statusCode === 408 || - statusCode === 429 || - statusCode === 500 || - statusCode === 502 || - statusCode === 503 || - statusCode === 504 - ); - } - - // Will use the conversations.list API (using fetch) to find the channel ID - // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ") - async #findChannelId( - service: HttpService, - params: z.infer, - cache?: CacheService - ): Promise { - if ("channelId" in params) { - return params.channelId; - } - - if (!("channelName" in params)) { - throw new Error("Invalid params, mising channelId and channelName"); - } - - //if the channelName starts with a #, remove it - if (params.channelName.startsWith("#")) { - params.channelName = params.channelName.substring(1); - } - - const cachedChannelId = await cache?.get(params.channelName); - - if (cachedChannelId) { - return cachedChannelId; - } - - const response = await service.performRequest( - this.#listConversationsEndpoint - ); - - if (response.success && response.data.ok) { - const { channels } = response.data; - - const channelInfo = channels.find( - (c: any) => c.name === params.channelName - ); - - if (channelInfo) { - await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24); - return channelInfo.id; - } - } - - return undefined; - } -} - -export const requests = new SlackRequestIntegration(); +export { requests }; diff --git a/packages/internal-integrations/src/slack/requests.ts b/packages/internal-integrations/src/slack/requests.ts new file mode 100644 index 00000000000..163bd77a6eb --- /dev/null +++ b/packages/internal-integrations/src/slack/requests.ts @@ -0,0 +1,467 @@ +import { HttpEndpoint, HttpService } from "../services"; +import { + DisplayProperties, + CacheService, + PerformedRequestResponse, + PerformRequestOptions, + RequestIntegration, + AccessInfo, +} from "../types"; +import { slack } from "@trigger.dev/providers"; +import debug from "debug"; +import { getAccessToken } from "../accessInfo"; +import { z } from "zod"; +import type { ReactNode } from "react"; + +const log = debug("trigger:integrations:slack"); + +const SendSlackMessageRequestBodySchema = + slack.schemas.PostMessageBodySchema.extend({ + link_names: z.literal(1), + metadata: z + .object({ event_type: z.string(), event_payload: z.any() }) + .optional(), + }); + +class SlackRequestIntegration implements RequestIntegration { + #joinChannelEndpoint = new HttpEndpoint< + typeof slack.schemas.JoinConversationResponseSchema, + typeof slack.schemas.JoinConversationBodySchema + >({ + response: slack.schemas.JoinConversationResponseSchema, + method: "POST", + path: "/conversations.join", + }); + + #listConversationsEndpoint = new HttpEndpoint({ + response: slack.schemas.ListConversationsResponseSchema, + method: "GET", + path: "/conversations.list", + }); + + #postMessageEndpoint = new HttpEndpoint< + typeof slack.schemas.PostMessageResponseSchema, + typeof SendSlackMessageRequestBodySchema + >({ + response: slack.schemas.PostMessageResponseSchema, + method: "POST", + path: "/chat.postMessage", + }); + + #addReactionEndpoint = new HttpEndpoint< + typeof slack.schemas.AddReactionResponseSchema, + typeof slack.schemas.AddReactionOptionsSchema + >({ + response: slack.schemas.AddReactionResponseSchema, + method: "POST", + path: "/reactions.add", + }); + + constructor(private readonly baseUrl: string = "https://slack.com/api") {} + + perform(options: PerformRequestOptions): Promise { + switch (options.endpoint) { + case "chat.postMessage": { + return this.#postMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "chat.postMessageResponse": { + return this.#postMessageResponse( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "reactions.add": { + return this.#addReaction( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + default: { + throw new Error(`Unknown endpoint: ${options.endpoint}`); + } + } + } + + displayProperties(endpoint: string, params: any): DisplayProperties { + switch (endpoint) { + case "chat.postMessage": { + return { + title: `Post message to ${ + "channelName" in params ? params.channelName : params.channelId + }`, + properties: [ + { + key: "Text", + value: params.text, + }, + ], + }; + } + case "chat.postMessageResponse": { + return { + title: `Post response`, + properties: [], + }; + } + case "reactions.add": { + return { + title: `Add reaction to message ${params.timestamp}`, + properties: [ + { + key: "Reaction", + value: params.name, + }, + ], + }; + } + + default: { + throw new Error(`Unknown endpoint: ${endpoint}`); + } + } + } + + renderComponent(input: any, output: any): ReactNode { + return null; + } + + async #postMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params); + + log("chat.postMessage %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: this.baseUrl, + }); + + const channelId = await this.#findChannelId(service, params, cache); + + if (!channelId) { + return { + ok: false, + isRetryable: false, + response: { + output: { + message: `channelId not found`, + }, + context: { + statusCode: 404, + headers: {}, + }, + }, + }; + } + + log("found channelId %s", channelId); + + const response = await service.performRequest(this.#postMessageEndpoint, { + ...parsedParams, + link_names: 1, + channel: channelId, + metadata: metadata + ? { event_type: "post_message", event_payload: metadata } + : undefined, + }); + + if (!response.success) { + log("chat.postMessage failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + if (!response.data.ok && response.data.error === "not_in_channel") { + log( + "chat.postMessage failed with not_in_channel, attempting to join channel %s", + channelId + ); + + // Attempt to join the channel, and then retry the request + const joinResponse = await service.performRequest( + this.#joinChannelEndpoint, + { + channel: channelId, + } + ); + + if (joinResponse.success && joinResponse.data.ok) { + log("joined channel %s, retrying postMessage", channelId); + + return this.#postMessage(accessInfo, params); + } + } + + const ok = response.data.ok; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + async #addReaction( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = slack.schemas.AddReactionOptionsSchema.parse(params); + + log("reactions.add %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: this.baseUrl, + }); + + const channelId = await this.#findChannelId(service, parsedParams, cache); + + if (!channelId) { + return { + ok: false, + isRetryable: false, + response: { + output: { + message: `channelId not found`, + }, + context: { + statusCode: 404, + headers: {}, + }, + }, + }; + } + + log("found channelId %s", channelId); + + const response = await service.performRequest(this.#addReactionEndpoint, { + ...parsedParams, + // @ts-ignore + channel: channelId, + }); + + if (!response.success) { + log("reactions.add failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + if (!response.data.ok && response.data.error === "not_in_channel") { + log( + "reactions.add failed with not_in_channel, attempting to join channel %s", + channelId + ); + + // Attempt to join the channel, and then retry the request + const joinResponse = await service.performRequest( + this.#joinChannelEndpoint, + { + channel: channelId, + } + ); + + if (joinResponse.success && joinResponse.data.ok) { + log("joined channel %s, retrying reactions.add", channelId); + + return this.#addReaction(accessInfo, params); + } + } + + const ok = response.data.ok; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + async #postMessageResponse( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = z + .object({ + message: slack.schemas.PostMessageResponseOptionsSchema, + responseUrl: z.string(), + }) + .parse(params); + + log("chat.postMessageResponse %O", parsedParams); + + const response = await fetch(parsedParams.responseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...parsedParams.message, + metadata: metadata + ? { event_type: "post_message_response", event_payload: metadata } + : undefined, + }), + }); + + if (!response.ok) { + log("chat.postMessageResponse failed %O", response); + + const error = await safeGetJson(response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.status), + response: { + output: error + ? error + : { name: `${response.status}`, message: response.statusText }, + context: { + statusCode: response.status, + headers: response.headers, + }, + }, + }; + } + + const output = await safeGetJson(response); + + const performedRequest = { + ok: response.ok, + isRetryable: this.#isRetryable(response.status), + response: { + output, + context: { + statusCode: response.status, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + #isRetryable(statusCode: number): boolean { + return ( + statusCode === 408 || + statusCode === 429 || + statusCode === 500 || + statusCode === 502 || + statusCode === 503 || + statusCode === 504 + ); + } + + // Will use the conversations.list API (using fetch) to find the channel ID + // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ") + async #findChannelId( + service: HttpService, + params: z.infer, + cache?: CacheService + ): Promise { + if ("channelId" in params) { + return params.channelId; + } + + if (!("channelName" in params)) { + throw new Error("Invalid params, mising channelId and channelName"); + } + + //if the channelName starts with a #, remove it + if (params.channelName.startsWith("#")) { + params.channelName = params.channelName.substring(1); + } + + const cachedChannelId = await cache?.get(params.channelName); + + if (cachedChannelId) { + return cachedChannelId; + } + + const response = await service.performRequest( + this.#listConversationsEndpoint + ); + + if (response.success && response.data.ok) { + const { channels } = response.data; + + const channelInfo = channels.find( + (c: any) => c.name === params.channelName + ); + + if (channelInfo) { + await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24); + return channelInfo.id; + } + } + + return undefined; + } +} + +export const requests = new SlackRequestIntegration(); + +function safeGetJson(response: Response): Promise { + return response.json().catch(() => null); +} diff --git a/packages/internal-integrations/src/types.ts b/packages/internal-integrations/src/types.ts index c9006a3159b..68eba80be31 100644 --- a/packages/internal-integrations/src/types.ts +++ b/packages/internal-integrations/src/types.ts @@ -42,6 +42,7 @@ export type PerformRequestOptions = { endpoint: string; params: any; cache?: CacheService; + metadata?: Record; }; export type DisplayProperties = { diff --git a/packages/trigger-integrations/CHANGELOG.md b/packages/trigger-integrations/CHANGELOG.md index 1ee237b061a..37e8682b3f0 100644 --- a/packages/trigger-integrations/CHANGELOG.md +++ b/packages/trigger-integrations/CHANGELOG.md @@ -1,5 +1,15 @@ # @trigger.dev/integrations +## 0.1.15 + +### Patch Changes + +- b290410: Slack blocks support +- Updated dependencies [52d21ac] +- Updated dependencies [b290410] + - @trigger.dev/sdk@0.2.11 + - @trigger.dev/providers@0.1.9 + ## 0.1.14 ### Patch Changes diff --git a/packages/trigger-integrations/package.json b/packages/trigger-integrations/package.json index b3807920f08..e323c395721 100644 --- a/packages/trigger-integrations/package.json +++ b/packages/trigger-integrations/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/integrations", - "version": "0.1.14", + "version": "0.1.15", "description": "trigger.dev integrations", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-integrations/src/integrations/slack/events.ts b/packages/trigger-integrations/src/integrations/slack/events.ts new file mode 100644 index 00000000000..67dc5afb914 --- /dev/null +++ b/packages/trigger-integrations/src/integrations/slack/events.ts @@ -0,0 +1,37 @@ +import { slack } from "@trigger.dev/providers"; +import { TriggerEvent } from "@trigger.dev/sdk"; + +export function blockActionInteraction(params: { + blockId: string; + actionId?: string | string[]; +}): TriggerEvent { + const actionIds = + typeof params.actionId === "undefined" + ? [] + : Array.isArray(params.actionId) + ? params.actionId + : [params.actionId]; + + return { + metadata: { + type: "SLACK_INTERACTION", + service: "slack", + name: "block.action", + filter: { + service: ["slack"], + payload: { + actions: { + block_id: [params.blockId], + action_id: actionIds, + }, + }, + event: ["block.action"], + }, + source: { + blockId: params.blockId, + actionIds, + }, + }, + schema: slack.schemas.blockAction, + }; +} diff --git a/packages/trigger-integrations/src/integrations/slack/index.ts b/packages/trigger-integrations/src/integrations/slack/index.ts index 1e457edcc3e..fe1fd2e2005 100644 --- a/packages/trigger-integrations/src/integrations/slack/index.ts +++ b/packages/trigger-integrations/src/integrations/slack/index.ts @@ -1,6 +1,9 @@ import { getTriggerRun } from "@trigger.dev/sdk"; import { z } from "zod"; import { slack } from "@trigger.dev/providers"; +import * as events from "./events"; + +export { events }; export type PostMessageOptions = z.infer< typeof slack.schemas.PostMessageOptionsSchema @@ -31,3 +34,64 @@ export async function postMessage( return output; } + +export type PostMessageResponseOptions = z.infer< + typeof slack.schemas.PostMessageResponseOptionsSchema +>; + +export type PostMessageResponseResponse = z.infer< + typeof slack.schemas.PostMessageResponseSuccessResponseSchema +>; + +export async function postMessageResponse( + key: string, + responseUrl: string, + message: PostMessageResponseOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call postMessageResponse outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "slack", + endpoint: "chat.postMessageResponse", + params: { message, responseUrl }, + response: { + schema: slack.schemas.PostMessageResponseSuccessResponseSchema, + }, + }); + + return output; +} + +export type AddReactionOptions = z.infer< + typeof slack.schemas.AddReactionOptionsSchema +>; + +export type AddReactionResponse = z.infer< + typeof slack.schemas.AddReactionSuccessResponseSchema +>; + +export async function addReaction( + key: string, + options: AddReactionOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call addReaction outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "slack", + endpoint: "reactions.add", + params: options, + response: { + schema: slack.schemas.AddReactionSuccessResponseSchema, + }, + }); + + return output; +} diff --git a/packages/trigger-providers/CHANGELOG.md b/packages/trigger-providers/CHANGELOG.md index dba9c242ee6..a021c9863b1 100644 --- a/packages/trigger-providers/CHANGELOG.md +++ b/packages/trigger-providers/CHANGELOG.md @@ -1,5 +1,11 @@ # @trigger.dev/providers +## 0.1.9 + +### Patch Changes + +- b290410: Slack blocks support + ## 0.1.8 ### Patch Changes diff --git a/packages/trigger-providers/package.json b/packages/trigger-providers/package.json index a3ebac3991f..bb8f5d49682 100644 --- a/packages/trigger-providers/package.json +++ b/packages/trigger-providers/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/providers", - "version": "0.1.8", + "version": "0.1.9", "description": "trigger.dev API providers with schemas", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -23,6 +23,7 @@ "@types/react": "^18.0.26", "rimraf": "^3.0.2", "tsup": "^6.5.0", + "type-fest": "^3.5.3", "typescript": "^4.9.4" }, "scripts": { diff --git a/packages/trigger-providers/src/providers/slack/blocks.ts b/packages/trigger-providers/src/providers/slack/blocks.ts new file mode 100644 index 00000000000..514b9725df9 --- /dev/null +++ b/packages/trigger-providers/src/providers/slack/blocks.ts @@ -0,0 +1,861 @@ +import { z } from "zod"; + +export const imageElementSchema = z.object({ + type: z.literal("image"), + image_url: z.string(), + alt_text: z.string(), +}); + +export const plainTextElementSchema = z.object({ + type: z.literal("plain_text"), + text: z.string(), + emoji: z.boolean().optional(), +}); + +export const mrkdwnElementSchema = z.object({ + type: z.literal("mrkdwn"), + text: z.string(), + verbatim: z.boolean().optional(), +}); + +export const mrkdwnOptionSchema = z.object({ + text: mrkdwnElementSchema, + value: z.string().optional(), + url: z.string().optional(), + description: plainTextElementSchema.optional(), +}); + +export const plainTextOptionSchema = z.object({ + text: plainTextElementSchema, + value: z.string().optional(), + url: z.string().optional(), + description: plainTextElementSchema.optional(), +}); + +export const optionSchema = z.union([ + mrkdwnOptionSchema, + plainTextOptionSchema, +]); + +export const confirmSchema = z.object({ + title: plainTextElementSchema.optional(), + text: z.discriminatedUnion("type", [ + plainTextElementSchema, + mrkdwnElementSchema, + ]), + confirm: plainTextElementSchema.optional(), + deny: plainTextElementSchema.optional(), + style: z.union([z.literal("primary"), z.literal("danger")]).optional(), +}); + +/** + * @description Determines when an input element will return a + * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` interaction payload}. + */ +export const dispatchActionConfigSchema = z.object({ + /** + * @description An array of interaction types that you would like to receive a + * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload} for. Should be + * one or both of: + * `on_enter_pressed` — payload is dispatched when user presses the enter key while the input is in focus. Hint + * text will appear underneath the input explaining to the user to press enter to submit. + * `on_character_entered` — payload is dispatched when a character is entered (or removed) in the input. + */ + trigger_actions_on: z + .array( + z.union([ + z.literal("on_enter_pressed"), + z.literal("on_character_entered"), + ]) + ) + .optional(), +}); + +export const actionSchema = z.object({ + type: z.string(), + /** + * @description: An identifier for this action. You can use this when you receive an interaction payload to + * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique + * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. + */ + action_id: z.string().optional(), +}); + +export const actionIdSchema = z.object({ + /** + * @description: An identifier for this action. You can use this when you receive an interaction payload to + * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique + * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. + */ + action_id: z.string().optional(), +}); + +export const confirmableSchema = z.object({ + /** + * @description A {@see Confirm} object that defines an optional confirmation dialog after the element is interacted + * with. + */ + confirm: confirmSchema.optional(), +}); + +export const focusableSchema = z.object({ + /** + * @description Indicates whether the element will be set to auto focus within the + * {@link https://api.slack.com/reference/surfaces/views `view` object}. Only one element can be set to `true`. + * Defaults to `false`. + */ + focus_on_load: z.boolean().optional(), +}); + +export const placeholdableSchema = z.object({ + /** + * @description A {@see PlainTextElement} object that defines the placeholder text shown on the element. Maximum + * length for the `text` field in this object is 150 characters. + */ + placeholder: plainTextElementSchema.optional(), +}); + +export const dispatchableSchema = z.object({ + /** + * @description A {@see DispatchActionConfig} object that determines when during text input the element returns a + * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload}. + */ + dispatch_action_config: dispatchActionConfigSchema.optional(), +}); + +export const usersSelectSchema = z + .object({ + type: z.literal("users_select"), + initial_user: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiUsersSelectSchema = z + .object({ + type: z.literal("multi_users_select"), + initial_users: z.array(z.string()).optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const staticSelectSchema = z + .object({ + type: z.literal("static_select"), + initial_option: plainTextOptionSchema.optional(), + options: z.array(plainTextOptionSchema).optional(), + option_groups: z + .array( + z.object({ + label: plainTextElementSchema, + options: z.array(plainTextOptionSchema), + }) + ) + .optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiStaticSelectSchema = z + .object({ + type: z.literal("multi_static_select"), + initial_options: z.array(plainTextOptionSchema).optional(), + options: z.array(plainTextOptionSchema).optional(), + option_groups: z + .array( + z.object({ + label: plainTextElementSchema, + options: z.array(plainTextOptionSchema), + }) + ) + .optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const conversationsSelectSchema = z + .object({ + type: z.literal("conversations_select"), + initial_conversation: z.string().optional(), + response_url_enabled: z.boolean().optional(), + default_to_current_conversation: z.boolean().optional(), + filter: z + .object({ + include: z + .array( + z.union([ + z.literal("im"), + z.literal("mpim"), + z.literal("private"), + z.literal("public"), + ]) + ) + .optional(), + exclude_external_shared_channels: z.boolean().optional(), + exclude_bot_users: z.boolean().optional(), + }) + .optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiConversationsSelectSchema = z + .object({ + type: z.literal("multi_conversations_select"), + initial_conversations: z.array(z.string()).optional(), + max_selected_items: z.number().optional(), + default_to_current_conversation: z.boolean().optional(), + filter: z + .object({ + include: z + .array( + z.union([ + z.literal("im"), + z.literal("mpim"), + z.literal("private"), + z.literal("public"), + ]) + ) + .optional(), + exclude_external_shared_channels: z.boolean().optional(), + exclude_bot_users: z.boolean().optional(), + }) + .optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const channelsSelectSchema = z + .object({ + type: z.literal("channels_select"), + initial_channel: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiChannelsSelectSchema = z + .object({ + type: z.literal("multi_channels_select"), + initial_channels: z.array(z.string()).optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const externalSelectSchema = z + .object({ + type: z.literal("external_select"), + initial_option: plainTextOptionSchema.optional(), + min_query_length: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiExternalSelectSchema = z + .object({ + type: z.literal("multi_external_select"), + initial_options: z.array(plainTextOptionSchema).optional(), + min_query_length: z.number().optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const buttonSchema = z + .object({ + type: z.literal("button"), + text: plainTextElementSchema, + value: z.string().optional(), + url: z.string().optional(), + style: z.union([z.literal("danger"), z.literal("primary")]).optional(), + accessibility_label: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema); + +export const overflowSchema = z + .object({ + type: z.literal("overflow"), + options: z.array(plainTextOptionSchema), + }) + .merge(actionIdSchema) + .merge(confirmableSchema); + +export const datepickerSchema = z + .object({ + type: z.literal("datepicker"), + initial_date: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const timepickerSchema = z + .object({ + type: z.literal("timepicker"), + initial_time: z.string().optional(), + timezone: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const radioButtonsSchema = z + .object({ + type: z.literal("radio_buttons"), + initial_option: optionSchema.optional(), + options: z.array(optionSchema), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema); + +/** + * @description An element that allows the selection of a time of day formatted as a UNIX timestamp. On desktop + * clients, this time picker will take the form of a dropdown list merge the date picker will take the form of a dropdown + * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker merge date + * picker will use native UIs. + * {@link https://api.slack.com/reference/block-kit/block-elements#datetimepicker} + */ +export const dateTimepickerSchema = z + .object({ + type: z.literal("datetimepicker"), + /** + * @description The initial date merge time that is selected when the element is loaded, represented as a UNIX + * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date merge + * time August 10th, 2021 at 03:17pm PST. + */ + initial_date_time: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema); + +export const checkboxesSchema = z + .object({ + type: z.literal("checkboxes"), + initial_options: z.array(optionSchema).optional(), + options: z.array(optionSchema), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema); + +export const plainTextInputSchema = z + .object({ + type: z.literal("plain_text_input"), + initial_value: z.string().optional(), + multiline: z.boolean().optional(), + min_length: z.number().optional(), + max_length: z.number().optional(), + dispatch_action_config: dispatchActionConfigSchema.optional(), + focus_on_load: z.boolean().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +/** + * @description A URL input element, similar to the {@see PlainTextInput} element, creates a single line field where + * a user can enter URL-encoded data. + * {@link https://api.slack.com/reference/block-kit/block-elements#url} + */ +export const uRLInputSchema = z + .object({ + type: z.literal("url_text_input"), + /** + * @description The initial value in the URL input when it is loaded. + */ + initial_value: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +/** + * @description An email input element, similar to the {@see PlainTextInput} element, creates a single line field where + * a user can enter an email address. + * {@link https://api.slack.com/reference/block-kit/block-elements#email} + */ +export const emailInputSchema = z + .object({ + type: z.literal("email_text_input"), + /** + * @description The initial value in the email input when it is loaded. + */ + initial_value: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +/** + * @description A number input element, similar to the {@see PlainTextInput} element, creates a single line field where + * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, merge -10 are all + * valid input values. Decimal numbers are only allowed when `is_decimal_allowed` is equal to `true`. + * {@link https://api.slack.com/reference/block-kit/block-elements#number} + */ +export const numberInputSchema = z + .object({ + type: z.literal("number_input"), + /** + * @description Decimal numbers are allowed if this property is `true`, set the value to `false` otherwise. + */ + is_decimal_allowed: z.boolean(), + /** + * @description The initial value in the input when it is loaded. + */ + initial_value: z.string().optional(), + /** + * @description The minimum value, cannot be greater than `max_value`. + */ + min_value: z.string().optional(), + /** + * @description The maximum value, cannot be less than `min_value`. + */ + max_value: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const blockSchema = z.object({ + type: z.string(), + block_id: z.string().optional(), +}); + +export const imageBlockSchema = blockSchema.extend({ + type: z.literal("image"), + image_url: z.string(), + alt_text: z.string(), + title: plainTextElementSchema.optional(), +}); + +export const contextBlockSchema = blockSchema.extend({ + type: z.literal("context"), + elements: z.array( + z.discriminatedUnion("type", [ + imageElementSchema, + plainTextElementSchema, + mrkdwnElementSchema, + ]) + ), +}); + +export const dividerBlockSchema = blockSchema.extend({ + type: z.literal("divider"), +}); + +export const fileBlockSchema = blockSchema.extend({ + type: z.literal("file"), + source: z.string(), + external_id: z.string(), +}); + +export const headerBlockSchema = blockSchema.extend({ + type: z.literal("header"), + text: plainTextElementSchema, +}); + +export const messageMetadataEventPayloadObjectSchema = z.record( + z.union([z.string(), z.number(), z.boolean()]) +); + +export const messageAttachmentPreviewSchema = z.object({ + type: z.string().optional(), + can_remove: z.boolean().optional(), + title: plainTextElementSchema.optional(), + subtitle: plainTextElementSchema.optional(), + iconUrl: z.string().optional(), +}); + +export const optionFieldSchema = z.object({ + description: z.string().optional(), + text: z.string(), + value: z.string(), +}); + +export const confirmationSchema = z.object({ + dismiss_text: z.string().optional(), + ok_text: z.string().optional(), + text: z.string(), + title: z.string().optional(), +}); + +export const selectOptionSchema = z.object({ + label: z.string(), + value: z.string(), +}); + +export const callUserSlackSchema = z.object({ + slack_id: z.string(), +}); + +export const callUserExternalSchema = z.object({ + external_id: z.string(), + display_name: z.string(), + avatar_url: z.string(), +}); + +export const videoBlockSchema = blockSchema.extend({ + type: z.literal("video"), + video_url: z.string(), + thumbnail_url: z.string(), + alt_text: z.string(), + title: plainTextElementSchema, + title_url: z.string().optional(), + author_name: z.string().optional(), + provider_name: z.string().optional(), + provider_icon_url: z.string().optional(), + description: plainTextElementSchema.optional(), +}); + +export const dialogSchema = z.object({ + title: z.string(), + callback_id: z.string(), + elements: z.array( + z.object({ + type: z.union([ + z.literal("text"), + z.literal("textarea"), + z.literal("select"), + ]), + name: z.string(), + label: z.string(), + optional: z.boolean().optional(), + placeholder: z.string().optional(), + value: z.string().optional(), + max_length: z.number().optional(), + min_length: z.number().optional(), + hint: z.string().optional(), + subtype: z + .union([ + z.literal("email"), + z.literal("number"), + z.literal("tel"), + z.literal("url"), + ]) + .optional(), + data_source: z + .union([ + z.literal("users"), + z.literal("channels"), + z.literal("conversations"), + z.literal("external"), + ]) + .optional(), + selected_options: z.array(selectOptionSchema).optional(), + options: z.array(selectOptionSchema).optional(), + option_groups: z + .array( + z.object({ + label: z.string(), + options: z.array(selectOptionSchema), + }) + ) + .optional(), + min_query_length: z.number().optional(), + }) + ), + submit_label: z.string().optional(), + notify_on_cancel: z.boolean().optional(), + state: z.string().optional(), +}); + +const selectSchemas = [ + usersSelectSchema, + staticSelectSchema, + conversationsSelectSchema, + channelsSelectSchema, + externalSelectSchema, +]; + +export const selectSchema = z.discriminatedUnion("type", [ + usersSelectSchema, + staticSelectSchema, + conversationsSelectSchema, + channelsSelectSchema, + externalSelectSchema, +]); + +const multiSelectSchemas = [ + multiUsersSelectSchema, + multiStaticSelectSchema, + multiConversationsSelectSchema, + multiChannelsSelectSchema, + multiExternalSelectSchema, +]; +export const multiSelectSchema = z.discriminatedUnion("type", [ + multiUsersSelectSchema, + multiStaticSelectSchema, + multiConversationsSelectSchema, + multiChannelsSelectSchema, + multiExternalSelectSchema, +]); + +export const actionsBlockSchema = blockSchema.extend({ + type: z.literal("actions"), + elements: z.array( + z.discriminatedUnion("type", [ + buttonSchema, + overflowSchema, + datepickerSchema, + timepickerSchema, + dateTimepickerSchema, + ...selectSchemas, + radioButtonsSchema, + checkboxesSchema, + ]) + ), +}); + +export const sectionBlockSchema = blockSchema.extend({ + type: z.literal("section"), + text: z + .discriminatedUnion("type", [plainTextElementSchema, mrkdwnElementSchema]) + .optional(), + fields: z + .array( + z.discriminatedUnion("type", [ + plainTextElementSchema, + mrkdwnElementSchema, + ]) + ) + .optional(), + accessory: z + .discriminatedUnion("type", [ + buttonSchema, + overflowSchema, + datepickerSchema, + timepickerSchema, + ...selectSchemas, + ...multiSelectSchemas, + imageElementSchema, + radioButtonsSchema, + checkboxesSchema, + ]) + .optional(), +}); + +export const inputBlockSchema = blockSchema.extend({ + type: z.literal("input"), + label: plainTextElementSchema, + hint: plainTextElementSchema.optional(), + optional: z.boolean().optional(), + element: z.discriminatedUnion("type", [ + datepickerSchema, + ...selectSchemas, + ...multiSelectSchemas, + timepickerSchema, + dateTimepickerSchema, + plainTextInputSchema, + uRLInputSchema, + emailInputSchema, + numberInputSchema, + radioButtonsSchema, + checkboxesSchema, + ]), + dispatch_action: z.boolean().optional(), +}); + +export const messageMetadataSchema = z.object({ + event_type: z.string(), + event_payload: z.record( + z.union([ + z.string(), + z.number(), + z.boolean(), + messageMetadataEventPayloadObjectSchema, + z.array(messageMetadataEventPayloadObjectSchema), + ]) + ), +}); + +export const attachmentActionSchema = z.object({ + id: z.string().optional(), + confirm: confirmationSchema.optional(), + data_source: z + .union([ + z.literal("static"), + z.literal("channels"), + z.literal("conversations"), + z.literal("users"), + z.literal("external"), + ]) + .optional(), + min_query_length: z.number().optional(), + name: z.string().optional(), + options: z.array(optionFieldSchema).optional(), + option_groups: z + .array( + z.object({ + text: z.string(), + options: z.array(optionFieldSchema), + }) + ) + .optional(), + selected_options: z.array(optionFieldSchema).optional(), + style: z + .union([z.literal("default"), z.literal("primary"), z.literal("danger")]) + .optional(), + text: z.string(), + type: z.union([z.literal("button"), z.literal("select")]), + value: z.string().optional(), + url: z.string().optional(), +}); + +export const callUserSchema = z.union([ + callUserSlackSchema, + callUserExternalSchema, +]); + +const knownBlocks = [ + imageBlockSchema, + contextBlockSchema, + actionsBlockSchema, + dividerBlockSchema, + sectionBlockSchema, + inputBlockSchema, + fileBlockSchema, + headerBlockSchema, + videoBlockSchema, +]; + +export const knownBlockSchema = z.discriminatedUnion("type", [ + imageBlockSchema, + contextBlockSchema, + actionsBlockSchema, + dividerBlockSchema, + sectionBlockSchema, + inputBlockSchema, + fileBlockSchema, + headerBlockSchema, + videoBlockSchema, +]); + +const anyBlockSchema = z.discriminatedUnion("type", [ + imageBlockSchema, + contextBlockSchema, + actionsBlockSchema, + dividerBlockSchema, + sectionBlockSchema, + inputBlockSchema, + fileBlockSchema, + headerBlockSchema, + videoBlockSchema, +]); + +export const messageAttachmentSchema = z.object({ + blocks: z.array(anyBlockSchema).optional(), + fallback: z.string().optional(), + color: z + .union([ + z.literal("good"), + z.literal("warning"), + z.literal("danger"), + z.string(), + ]) + .optional(), + pretext: z.string().optional(), + author_name: z.string().optional(), + author_link: z.string().optional(), + author_icon: z.string().optional(), + title: z.string().optional(), + title_link: z.string().optional(), + text: z.string().optional(), + fields: z + .array( + z.object({ + title: z.string(), + value: z.string(), + short: z.boolean().optional(), + }) + ) + .optional(), + image_url: z.string().optional(), + thumb_url: z.string().optional(), + footer: z.string().optional(), + footer_icon: z.string().optional(), + ts: z.string().optional(), + actions: z.array(attachmentActionSchema).optional(), + callback_id: z.string().optional(), + mrkdwn_in: z + .array( + z.union([z.literal("pretext"), z.literal("text"), z.literal("fields")]) + ) + .optional(), + app_unfurl_url: z.string().optional(), + is_app_unfurl: z.boolean().optional(), + app_id: z.string().optional(), + bot_id: z.string().optional(), + preview: messageAttachmentPreviewSchema.optional(), +}); + +export const linkUnfurlsSchema = z.record(messageAttachmentSchema); + +export const homeViewSchema = z.object({ + type: z.literal("home"), + blocks: z.array(anyBlockSchema), + private_metadata: z.string().optional(), + callback_id: z.string().optional(), + external_id: z.string().optional(), +}); + +export const modalViewSchema = z.object({ + type: z.literal("modal"), + title: plainTextElementSchema, + blocks: z.array(anyBlockSchema), + close: plainTextElementSchema.optional(), + submit: plainTextElementSchema.optional(), + private_metadata: z.string().optional(), + callback_id: z.string().optional(), + clear_on_close: z.boolean().optional(), + notify_on_close: z.boolean().optional(), + external_id: z.string().optional(), +}); + +export const workflowStepViewSchema = z.object({ + type: z.literal("workflow_step"), + blocks: z.array(anyBlockSchema), + private_metadata: z.string().optional(), + callback_id: z.string().optional(), + submit_disabled: z.boolean().optional(), + external_id: z.string().optional(), +}); + +export const viewSchema: z.ZodDiscriminatedUnion< + "type", + [typeof homeViewSchema, typeof modalViewSchema, typeof workflowStepViewSchema] +> = z.discriminatedUnion("type", [ + homeViewSchema, + modalViewSchema, + workflowStepViewSchema, +]); diff --git a/packages/trigger-providers/src/providers/slack/index.ts b/packages/trigger-providers/src/providers/slack/index.ts index 03461c695a3..51121adba63 100644 --- a/packages/trigger-providers/src/providers/slack/index.ts +++ b/packages/trigger-providers/src/providers/slack/index.ts @@ -15,6 +15,8 @@ export const slack = { "groups:write", "im:write", "mpim:write", + "chat:write.customize", + "reactions:write", ], }, schemas, diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts new file mode 100644 index 00000000000..2e341d605e3 --- /dev/null +++ b/packages/trigger-providers/src/providers/slack/interactivity.ts @@ -0,0 +1,215 @@ +import { z } from "zod"; +import { + knownBlockSchema, + mrkdwnElementSchema, + optionFieldSchema, + plainTextElementSchema, + viewSchema, +} from "./blocks"; + +const textSchema = z.discriminatedUnion("type", [ + plainTextElementSchema, + mrkdwnElementSchema, +]); +const blockActionType = z.union([ + z.literal("block_actions"), + z.literal("interactive_message"), +]); + +const sourceType = z.literal("message"); + +const commonActionSchema = z.object({ + action_id: z.string(), + block_id: z.string(), + action_ts: z.string(), +}); + +const buttonAction = z.object({ + type: z.literal("button"), + text: textSchema.optional(), + value: z.string(), +}); + +const selectedOptionSchema = z.object({ + text: z.object({ + type: z.string(), + text: z.string(), + emoji: z.boolean().optional(), + }), + value: z.string(), +}); +const placeholderSchema = z.object({ + type: z.string(), + text: z.string(), + emoji: z.boolean(), +}); +const staticSelectAction = z.object({ + type: z.literal("static_select"), + selected_option: selectedOptionSchema.nullable(), + placeholder: placeholderSchema.optional(), +}); + +const userSelectAction = z.object({ + type: z.literal("users_select"), + selected_user: z.string().nullable(), + initial_user: z.string().optional(), +}); + +const conversationsSelectAction = z.object({ + type: z.literal("conversations_select"), + selected_conversation: z.string().nullable(), + initial_conversation: z.string().optional(), +}); +const channelSelectAction = z.object({ + type: z.literal("channels_select"), + selected_channel: z.string().nullable(), + initial_channel: z.string().optional(), +}); + +const datePickerAction = z.object({ + type: z.literal("datepicker"), + selected_date: z.string().nullable(), + initial_date: z.string().optional(), +}); + +const checkboxesAction = z.object({ + type: z.literal("checkboxes"), + selected_options: z.array(optionFieldSchema), +}); + +const radioButtonsSchema = z.object({ + type: z.literal("radio_buttons"), + selectedOption: optionFieldSchema, +}); + +const timePickerSchema = z.object({ + type: z.literal("timepicker"), + selected_time: z.string().nullable(), + initial_time: z.string().optional(), +}); + +const plainTextInputSchema = z.object({ + type: z.literal("plain_text_input"), + value: z.string().nullable(), + initial_value: z.string().optional(), +}); + +const multiUsersSelectSchema = z.object({ + type: z.literal("multi_users_select"), + selected_users: z.array(z.string()), + initial_users: z.array(z.string()).optional(), +}); + +const multiStaticSelectSchema = z.object({ + type: z.literal("multi_static_select"), + selected_options: z.array(selectedOptionSchema), + placeholder: placeholderSchema.optional(), +}); + +const possibleActionsSchema = z.discriminatedUnion("type", [ + buttonAction, + staticSelectAction, + userSelectAction, + conversationsSelectAction, + channelSelectAction, + datePickerAction, + checkboxesAction, + radioButtonsSchema, + timePickerSchema, + plainTextInputSchema, + multiUsersSelectSchema, + multiStaticSelectSchema, +]); + +const actionSchema = possibleActionsSchema.and(commonActionSchema); + +//state.values.issue.action.block.rating.selected_option +const stateSchema = z.object({ + values: z.record(z.record(possibleActionsSchema)), +}); + +const userSchema = z.object({ + id: z.string(), + username: z.string(), + name: z.string(), + team_id: z.string(), +}); + +const containerSchema = z.object({ + type: sourceType, + message_ts: z.string(), + channel_id: z.string(), + is_ephemeral: z.boolean(), +}); + +const teamSchema = z.object({ id: z.string(), domain: z.string() }); +const channelSchema = z.object({ id: z.string(), name: z.string() }); + +const viewActionDataSchema = z.object({ + id: z.string(), + team_id: z.string(), + state: z + .object({ + values: z.record(z.record(z.any())), + }) + .optional(), + hash: z.string(), + previous_view_id: z.string().optional(), + root_view_id: z.string().optional(), + app_id: z.string().optional(), + app_installed_team_id: z.string().optional(), + bot_id: z.string().optional(), +}); +const viewActionSchema: Zod.ZodIntersection< + typeof viewSchema, + typeof viewActionDataSchema +> = viewSchema.and(viewActionDataSchema); + +const messageActionSchema = z.object({ + bot_id: z.string(), + type: sourceType, + text: z.string().optional(), + user: z.string().optional(), + ts: z.string(), + app_id: z.string().optional(), + blocks: z.array(knownBlockSchema).optional(), + team: z.string().optional(), + metadata: z + .object({ + event_type: z.string(), + event_payload: z.object({ requestId: z.string() }), + }) + .optional(), +}); + +export const blockAction: Zod.ZodObject<{ + type: typeof blockActionType; + user: typeof userSchema; + api_app_id: Zod.ZodString; + container: typeof containerSchema; + trigger_id: z.ZodOptional; + team: typeof teamSchema; + enterprise: Zod.ZodAny; + is_enterprise_install: Zod.ZodBoolean; + channel: typeof channelSchema; + view: z.ZodOptional; + message: z.ZodOptional; + state: z.ZodOptional; + response_url: Zod.ZodString; + actions: Zod.ZodArray; +}> = z.object({ + type: blockActionType, + user: userSchema, + api_app_id: z.string(), + container: containerSchema, + trigger_id: z.string().optional(), + team: teamSchema, + enterprise: z.any(), + is_enterprise_install: z.boolean(), + channel: channelSchema, + view: viewActionSchema.optional(), + message: messageActionSchema.optional(), + state: stateSchema.optional(), + response_url: z.string(), + actions: z.array(actionSchema), +}); diff --git a/packages/trigger-providers/src/providers/slack/prodmanifest.json b/packages/trigger-providers/src/providers/slack/prodmanifest.json index d67716a9202..2f63a247e93 100644 --- a/packages/trigger-providers/src/providers/slack/prodmanifest.json +++ b/packages/trigger-providers/src/providers/slack/prodmanifest.json @@ -18,10 +18,12 @@ "groups:read", "im:read", "mpim:read", - "chat:write" + "chat:write", + "reactions:write" ], "bot": [ - "chat:write" + "chat:write", + "reactions:write" ] } }, diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts index 0a2d2427f0c..c1fe80518cd 100644 --- a/packages/trigger-providers/src/providers/slack/schemas.ts +++ b/packages/trigger-providers/src/providers/slack/schemas.ts @@ -1,4 +1,8 @@ import { z } from "zod"; +import { knownBlockSchema } from "./blocks"; +import { blockAction } from "./interactivity"; + +export { blockAction }; export const PostMessageSuccessResponseSchema = z.object({ ok: z.literal(true), @@ -6,7 +10,7 @@ export const PostMessageSuccessResponseSchema = z.object({ ts: z.string(), message: z.object({ text: z.string(), - user: z.string(), + user: z.string().optional(), bot_id: z.string(), attachments: z.array(z.unknown()).optional(), type: z.string(), @@ -28,6 +32,10 @@ export const PostMessageResponseSchema = z.discriminatedUnion("ok", [ export const PostMessageBodySchema = z.object({ channel: z.string(), text: z.string(), + blocks: z.array(knownBlockSchema).optional(), + username: z.string().optional(), + icon_emoji: z.string().optional(), + icon_url: z.string().optional(), }); export const ChannelNameOrIdSchema = z.union([ @@ -38,6 +46,17 @@ export const ChannelNameOrIdSchema = z.union([ export const PostMessageOptionsSchema = z .object({ text: z.string(), + blocks: z.array(knownBlockSchema).optional(), + username: z.string().optional(), + icon_emoji: z.string().optional(), + icon_url: z.string().optional(), + }) + .and(ChannelNameOrIdSchema); + +export const AddReactionOptionsSchema = z + .object({ + name: z.string(), + timestamp: z.string(), }) .and(ChannelNameOrIdSchema); @@ -71,3 +90,30 @@ export const ListConversationsResponseSchema = z.discriminatedUnion("ok", [ ListConversationsSuccessResponseSchema, ErrorResponseSchema, ]); + +export const PostMessageResponseOptionsSchema = z.object({ + text: z.string().optional(), + blocks: z.array(knownBlockSchema).optional(), + response_type: z.enum(["in_channel"]).optional(), + replace_original: z.boolean().optional(), + delete_original: z.boolean().optional(), + thread_ts: z.string().optional(), +}); + +export const PostMessageResponseSuccessResponseSchema = z.object({ + ok: z.literal(true), +}); + +export const PostMessageResponseResponseSchema = z.discriminatedUnion("ok", [ + PostMessageResponseSuccessResponseSchema, + ErrorResponseSchema, +]); + +export const AddReactionSuccessResponseSchema = z.object({ + ok: z.literal(true), +}); + +export const AddReactionResponseSchema = z.discriminatedUnion("ok", [ + AddReactionSuccessResponseSchema, + ErrorResponseSchema, +]); diff --git a/packages/trigger-providers/src/schemaUtils.ts b/packages/trigger-providers/src/schemaUtils.ts new file mode 100644 index 00000000000..7d3ca62b851 --- /dev/null +++ b/packages/trigger-providers/src/schemaUtils.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; +import { CamelCasedPropertiesDeep, SnakeCasedPropertiesDeep } from "type-fest"; + +/** Converts a Zod Schema from snake_case to camelCase */ +export function snakeToCamel( + schema: z.ZodType +) { + return schema.transform( + (object) => + deepSnakeToCamel(object) as unknown as CamelCasedPropertiesDeep< + typeof object + > + ); +} + +/** Converts a Zod Schema from camelCase to snake_case */ +export function camelToSnake( + schema: z.ZodType +) { + return schema.transform( + (object) => + deepCamelToSnake(object) as unknown as SnakeCasedPropertiesDeep< + typeof object + > + ); +} + +function deepSnakeToCamel(o: any): T { + if (isObject(o)) { + const n = {}; + + Object.keys(o).forEach((k) => { + // @ts-ignore + n[keySnakeToCamel(k)] = deepSnakeToCamel(o[k]); + }); + + return n as T; + } else if (isArray(o)) { + return o.map((i: any) => { + return deepSnakeToCamel(i); + }); + } + + return o; +} + +function keySnakeToCamel(s: string): string { + return s.replace(/([_][a-z])/gi, ($1) => { + return $1.toUpperCase().replace("-", "").replace("_", ""); + }); +} + +function deepCamelToSnake(o: any): T { + if (isObject(o)) { + const n = {}; + + Object.keys(o).forEach((k) => { + // @ts-ignore + n[keyCamelToSnake(k)] = deepCamelToSnake(o[k]); + }); + + return n as T; + } else if (isArray(o)) { + return o.map((i: any) => { + return deepCamelToSnake(i); + }); + } + + return o; +} + +function keyCamelToSnake(s: string): string { + return s + .replace(/[\w]([A-Z])/g, function (m) { + return m[0] + "_" + m[1]; + }) + .toLowerCase(); +} + +function isArray(a: any): boolean { + return Array.isArray(a); +} + +function isObject(o: any): boolean { + return o === Object(o) && !isArray(o) && typeof o !== "function"; +} diff --git a/packages/trigger-sdk/CHANGELOG.md b/packages/trigger-sdk/CHANGELOG.md index 1b84f33cfdc..5d75a5f4ede 100644 --- a/packages/trigger-sdk/CHANGELOG.md +++ b/packages/trigger-sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # @trigger.dev/sdk +## 0.2.11 + +### Patch Changes + +- 52d21ac: Added support for delaying delivery when sending custom events +- b290410: Slack blocks support + ## 0.2.10 ### Patch Changes diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index ad5074fbc28..2870b334e03 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sdk", - "version": "0.2.10", + "version": "0.2.11", "description": "trigger.dev Node.JS SDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d6dadfd395..36d67f12f19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,33 +35,6 @@ importers: devDependencies: mintlify: 2.0.16 - apps/internal-triggers: - specifiers: - '@trigger.dev/integrations': ^0.1.13 - '@trigger.dev/sdk': ^0.2.10 - '@trigger.dev/tsconfig': workspace:* - '@types/node': '16' - '@types/pg': ^8.6.6 - dotenv: ^16.0.3 - pg: ^8.8.0 - rimraf: ^3.0.2 - tsup: ^6.5.0 - typescript: ^4.9.4 - zod: ^3.20.2 - dependencies: - '@trigger.dev/integrations': 0.1.13 - '@trigger.dev/sdk': 0.2.10 - pg: 8.8.0 - zod: 3.20.2 - devDependencies: - '@trigger.dev/tsconfig': link:../../config-packages/tsconfig - '@types/node': 16.18.11 - '@types/pg': 8.6.6 - dotenv: 16.0.3 - rimraf: 3.0.2 - tsup: 6.5.0_typescript@4.9.4 - typescript: 4.9.4 - apps/webapp: specifiers: '@aws-sdk/client-s3': ^3.186.0 @@ -211,7 +184,7 @@ importers: '@aws-sdk/client-s3': 3.245.0 '@aws-sdk/s3-request-presigner': 3.245.0 '@cfworker/json-schema': 1.12.5 - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 @@ -238,7 +211,7 @@ importers: '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/providers': link:../../packages/trigger-providers '@trigger.dev/sdk': link:../../packages/trigger-sdk - '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle + '@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne bcryptjs: 2.4.3 classnames: 2.3.2 clsx: 1.2.1 @@ -535,11 +508,13 @@ importers: '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' + jsx-slack: ^5.3.0 tsx: ^3.12.0 zod: ^3.20.2 dependencies: '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk + jsx-slack: 5.3.0 zod: 3.20.2 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -775,7 +750,7 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@react-email/render': 0.0.3 + '@react-email/render': 0.0.3_react@18.2.0 '@trigger.dev/providers': link:../trigger-providers '@trigger.dev/sdk': link:../trigger-sdk zod: 3.20.2 @@ -794,6 +769,7 @@ importers: rimraf: ^3.0.2 tiny-invariant: ^1.2.0 tsup: ^6.5.0 + type-fest: ^3.5.3 typescript: ^4.9.4 zod: ^3.20.2 dependencies: @@ -806,6 +782,7 @@ importers: '@types/react': 18.0.26 rimraf: 3.0.2 tsup: 6.5.0_typescript@4.9.4 + type-fest: 3.5.3 typescript: 4.9.4 packages/trigger-sdk: @@ -3480,12 +3457,13 @@ packages: prettier: 2.8.2 dev: false - /@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu: + /@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde: resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==} peerDependencies: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.3.2 '@codemirror/state': 6.2.0 @@ -3505,7 +3483,7 @@ packages: /@codemirror/lang-javascript/6.1.2: resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==} dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/state': 6.2.0 @@ -4703,16 +4681,6 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@react-email/render/0.0.3: - resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} - engines: {node: '>=18.0.0'} - dependencies: - pretty: 2.0.0 - react-dom: 18.2.0 - transitivePeerDependencies: - - react - dev: false - /@react-email/render/0.0.3_react@18.2.0: resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} engines: {node: '>=18.0.0'} @@ -4828,7 +4796,7 @@ packages: eslint: 8.31.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy - eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny + eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq eslint-plugin-jest: 26.9.0_ohsifnwenhmxgcp7mend4dnv74 eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0 @@ -5189,6 +5157,11 @@ packages: engines: {node: '>=10'} dev: true + /@slack/types/2.8.0: + resolution: {integrity: sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + dev: false + /@swc/core-darwin-arm64/1.3.26: resolution: {integrity: sha512-FWWflBfKRYrUJtko2xiedC5XCa31O75IZZqnTWuLpe9g3C5tnUuF3M8LSXZS/dn6wprome1MhtG9GMPkSYkhkg==} engines: {node: '>=10'} @@ -5410,48 +5383,6 @@ packages: engines: {node: '>= 6'} dev: true - /@trigger.dev/integrations/0.1.13: - resolution: {integrity: sha512-2CfQNGqdC82om8Zr+PAdZZWov5SPAAAuVGV39d/MTNFLHhJZmJx47nbc5nnT/ujPSkoRrvOolwl93rVeceYoYA==} - dependencies: - '@react-email/render': 0.0.3 - '@trigger.dev/providers': 0.1.7 - '@trigger.dev/sdk': 0.2.10 - zod: 3.20.2 - transitivePeerDependencies: - - bufferutil - - encoding - - react - - supports-color - - utf-8-validate - dev: false - - /@trigger.dev/providers/0.1.7: - resolution: {integrity: sha512-wy7bh/bOGNgYbpUOJz59BkFU/0e3U0p7/5rClYY52QgAXZ9Z6c4JfocCqmYRxh+/Lb18N/wUd+vo5V9YfoyeIQ==} - dependencies: - '@shopify/admin-graphql-api-utilities': 2.0.1 - tiny-invariant: 1.3.1 - zod: 3.20.2 - dev: false - - /@trigger.dev/sdk/0.2.10: - resolution: {integrity: sha512-768mZmXi2iLgUfRIKOlpU3q9wRF6anhSnKoToJnRRzyAX3o6Kw5GkBXyM2QVuB5rt6psgbD+M4RKDaTnLpn/wQ==} - dependencies: - debug: 4.3.4 - evt: 2.4.13 - node-fetch: 2.6.7 - slug: 6.1.0 - ulid: 2.3.0 - uuid: 9.0.0 - ws: 8.12.0 - zod: 3.20.2 - zod-error: 1.1.0 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - dev: false - /@tsconfig/node10/1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -5722,14 +5653,6 @@ packages: /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} - /@types/pg/8.6.6: - resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} - dependencies: - '@types/node': 18.11.18 - pg-protocol: 1.5.0 - pg-types: 2.2.0 - dev: true - /@types/prismjs/1.26.0: resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} dev: true @@ -5993,17 +5916,18 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: + /@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 @@ -6012,11 +5936,14 @@ packages: '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: + /@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: + '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: @@ -6025,13 +5952,14 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom - codemirror: 6.0.1 + '@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e + codemirror: 6.0.1_@lezer+common@1.0.2 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' + - '@codemirror/lint' - '@codemirror/search' dev: false @@ -6835,11 +6763,6 @@ packages: resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} engines: {node: '>=0.10'} - /buffer-writer/2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - dev: false - /buffer/5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -7347,16 +7270,18 @@ packages: engines: {node: '>=0.10.0'} dev: false - /codemirror/6.0.1: + /codemirror/6.0.1_@lezer+common@1.0.2: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 + transitivePeerDependencies: + - '@lezer/common' dev: false /collection-visit/1.0.0: @@ -8692,7 +8617,7 @@ packages: debug: 4.3.4 enhanced-resolve: 5.12.0 eslint: 8.31.0 - eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny + eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq get-tsconfig: 4.3.0 globby: 13.1.3 is-core-module: 2.11.0 @@ -8702,7 +8627,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq: + /eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -8727,7 +8652,6 @@ packages: debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy transitivePeerDependencies: - supports-color dev: true @@ -8752,7 +8676,7 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-import/2.27.4_2ac3tknkazjoq5fxmuugu665ny: + /eslint-plugin-import/2.27.4_qdjeohovcytra7xto5vgmxssaq: resolution: {integrity: sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==} engines: {node: '>=4'} peerDependencies: @@ -8770,7 +8694,7 @@ packages: doctrine: 2.1.0 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq + eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -11264,6 +11188,13 @@ packages: object.assign: 4.1.4 dev: true + /jsx-slack/5.3.0: + resolution: {integrity: sha512-xPYP8zLO31AKYiyuDJ88ykJzV/GvZUK+ktM4xE6ESIzrCICSd8nhrW/yaSyX45U+oD5LNTDUnhJvQe0LFDi63g==} + engines: {node: '>=14'} + dependencies: + '@slack/types': 2.8.0 + dev: false + /junk/3.1.0: resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} engines: {node: '>=8'} @@ -13193,10 +13124,6 @@ packages: semver: 6.3.0 dev: true - /packet-reader/1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - dev: false - /pako/0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} dev: true @@ -13355,59 +13282,6 @@ packages: is-reference: 3.0.1 dev: true - /pg-connection-string/2.5.0: - resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} - dev: false - - /pg-int8/1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - /pg-pool/3.5.2_pg@8.8.0: - resolution: {integrity: sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==} - peerDependencies: - pg: '>=8.0' - dependencies: - pg: 8.8.0 - dev: false - - /pg-protocol/1.5.0: - resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==} - - /pg-types/2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.0 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - /pg/8.8.0: - resolution: {integrity: sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 - pg-connection-string: 2.5.0 - pg-pool: 3.5.2_pg@8.8.0 - pg-protocol: 1.5.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - dev: false - - /pgpass/1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - dependencies: - split2: 4.1.0 - dev: false - /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -13561,24 +13435,6 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /postgres-array/2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - /postgres-bytea/1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} - engines: {node: '>=0.10.0'} - - /postgres-date/1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - /postgres-interval/1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - dependencies: - xtend: 4.0.2 - /posthog-js/1.39.4: resolution: {integrity: sha512-Elpf1gwyuObueXi89iH+9pP+WhpkiivP8Qwej4RzOLwSTa7Floaa4rgAw7rnCnX1PtRoJ3F0kqb6q9T+aZjRiA==} dependencies: @@ -13934,15 +13790,6 @@ packages: shallow-equal: 1.2.1 dev: false - /react-dom/18.2.0: - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - scheduler: 0.23.0 - dev: false - /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -15110,11 +14957,6 @@ packages: through: 2.3.8 dev: true - /split2/4.1.0: - resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} - engines: {node: '>= 10.x'} - dev: false - /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -16037,6 +15879,11 @@ packages: engines: {node: '>=14.16'} dev: true + /type-fest/3.5.3: + resolution: {integrity: sha512-V2+og4j/rWReWvaFrse3s9g2xvUv/K9Azm/xo6CjIuq7oeGqsoimC7+9/A3tfvNcbQf8RPSVj/HV81fB4DJrjA==} + engines: {node: '>=14.16'} + dev: true + /type-is/1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} From 0137dac540522ce66c93c443b3c2f3ba00233318 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 23:24:11 -0800 Subject: [PATCH 09/71] Revert "Revert "Merge pull request #25 from triggerdotdev/dev"" This reverts commit ba617fa7d7f1a21af95ecd9eda2e19e3ec9ee68e. --- .../apis/slack/actions/post-message.mdx | 17 ++++++++++++++++- .../orgs/$organizationSlug/__org/index.tsx | 7 +++++-- examples/send-to-slack/src/index.tsx | 2 ++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index e2689e35f05..3310f1fbb4b 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -127,9 +127,12 @@ In this examples there are two workflows: 1. Messages your team every minute (!) and asks them how they're doing. 2. Receives the interaction from users. It adds reactions when the buttons are pressed and posts a message when a user selects from the dropdown menu. +Please note, that to use `jsx-slack` you will need to add the `jsxImportSource` pragma to the top of your file. This is because `jsx-slack` is not a React library, but it uses the same syntax as React. Also, you might need to add `"jsx": "react-jsx"` to your `compilerOption` in `tsconfig.json`. The workflow file needs to be a `.jsx` or `.tsx` file (like when you use React). + -```typescript Blocks with interactivity +```typescript workflow.tsx +/** @jsxImportSource jsx-slack */ import { slack } from "@trigger.dev/integrations"; import JSXSlack, { Actions, @@ -207,6 +210,7 @@ new Trigger({ channelId: event.channel.id, }); } + break; } case "status-help": { //the user needs help so add an 🆘 emoji as a reaction @@ -217,6 +221,7 @@ new Trigger({ channelId: event.channel.id, }); } + break; } case "rating": { if (action.type != "static_select") { @@ -244,4 +249,14 @@ new Trigger({ }).listen(); ``` +```json tsconfig.json +{ + //other options + "compilerOptions": { + "jsx": "react-jsx" + //other options + } +} +``` + diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx index c051854f187..f73804499a9 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx @@ -184,15 +184,18 @@ function WorkflowList({ } function lastRunDescription(lastRun: WorkflowListItem["lastRun"]) { - if (lastRun === undefined) { + if (lastRun === null || lastRun === undefined) { return "Never"; } + if (lastRun.status === "SUCCESS") { if (lastRun.finishedAt) { return formatDateTime(lastRun.finishedAt); + } else { + return "Unknown"; } - throw new Error("lastRun.finishedAt is undefined"); } + return runStatusLabel(lastRun.status); } diff --git a/examples/send-to-slack/src/index.tsx b/examples/send-to-slack/src/index.tsx index 08dbcb62b7d..7953c4031dc 100644 --- a/examples/send-to-slack/src/index.tsx +++ b/examples/send-to-slack/src/index.tsx @@ -221,6 +221,7 @@ new Trigger({ channelId: event.channel.id, }); } + break; } case "status-help": { //the user needs help so add an 🆘 emoji as a reaction @@ -231,6 +232,7 @@ new Trigger({ channelId: event.channel.id, }); } + break; } case "rating": { if (action.type != "static_select") { From 1fc7020b843e2890f3d497c78b9214d5471d2a09 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 23:24:15 -0800 Subject: [PATCH 10/71] Revert "Revert "Merge pull request #26 from triggerdotdev/dev"" This reverts commit 35aedadfe98a8e010f9b47b7096d11c499cbd5e5. --- apps/webapp/app/utils.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/utils.ts b/apps/webapp/app/utils.ts index a099e857d2e..4469a11ebe1 100644 --- a/apps/webapp/app/utils.ts +++ b/apps/webapp/app/utils.ts @@ -97,11 +97,16 @@ export function formatDateTime( date: Date, style: "medium" | "long" = "medium" ): string { - switch (style) { - case "long": - return dateFormatterLong.format(date); - case "medium": - return dateFormatter.format(date); + try { + switch (style) { + case "long": + return dateFormatterLong.format(date); + case "medium": + return dateFormatter.format(date); + } + } catch (error) { + console.error(error); + return "Unknown"; } } From 3e9e743751108bd919c527ad076c116c6f06ee9a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 23:24:20 -0800 Subject: [PATCH 11/71] Revert "Revert "Merge pull request #27 from triggerdotdev/dev"" This reverts commit 888c8a7460b08797c1649fa62c8c59d0ff6f063d. --- .../app/components/CreateNewWorkflow.tsx | 18 +++++------------- apps/webapp/app/hooks/useOrganizations.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 591f8337cea..2e2d061888a 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -84,22 +84,14 @@ export function CreateNewWorkflowNoWorkflows() { Easily authenticate with APIs using the supported integrations below. If there's an integration we don't yet support,{" "} - - {" "} + let us know +
    {" "} and we'll add it. -
    {getProviders(false).map((provider) => ( (routeMatch.data.organizations); + return hydrateObject< + UseDataFunctionReturn["organizations"] + >(routeMatch.data.organizations); } export function useCurrentOrganization() { @@ -26,7 +26,7 @@ export function useCurrentOrganization() { } const result = hydrateObject< - PrismaReturnType + UseDataFunctionReturn["organization"] >(routeMatch.data.organization); if (result == null) return undefined; From 251b64ccf232cc178ef888ba7174027bc9e34572 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 27 Jan 2023 23:24:26 -0800 Subject: [PATCH 12/71] Revert "Revert "Merge pull request #28 from triggerdotdev/dev"" This reverts commit d6512b2ba51ab820b2b62e7df6eaf16acc862a76. --- apps/webapp/app/root.tsx | 26 ++++++++++++++++++++++++++ apps/webapp/package.json | 8 ++++---- pnpm-lock.yaml | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 1f51c81cd6f..83b085f73e4 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -22,6 +22,7 @@ import { withSentry } from "@sentry/remix"; import { env } from "./env.server"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { PrimaryLink } from "./components/primitives/Buttons"; +import { Body } from "./components/primitives/text/Body"; export const links: LinksFunction = () => { return [ @@ -78,6 +79,31 @@ export function CatchBoundary() { ); } +export function ErrorBoundary({ error }: { error: any }) { + console.error(error); + return ( + + + Oops! + + + + +
    +
    +

    Oh no!

    + {JSON.stringify(error)} + + Back home + +
    + +
    + + + ); +} + function App() { const { toastMessage, posthogProjectKey, user } = useTypedLoaderData(); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 3324602ee55..320ae8bad35 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -63,10 +63,9 @@ "@tailwindcss/forms": "^0.5.2", "@tanstack/react-table": "^8.0.0-alpha.87", "@trigger.dev/common-schemas": "workspace:*", + "@trigger.dev/integrations": "workspace:*", "@trigger.dev/providers": "workspace:*", "@trigger.dev/sdk": "workspace:*", - "@trigger.dev/integrations": "workspace:*", - "internal-bridge": "workspace:*", "@uiw/react-codemirror": "^4.13.2", "bcryptjs": "^2.4.3", "classnames": "^2.3.1", @@ -81,6 +80,7 @@ "emails": "workspace:*", "express": "^4.18.1", "humanize-duration": "^3.27.3", + "internal-bridge": "workspace:*", "internal-integrations": "workspace:*", "internal-platform": "workspace:*", "internal-pulsar": "workspace:*", @@ -112,7 +112,7 @@ "remix-auth": "^3.2.2", "remix-auth-email-link": "^1.4.2", "remix-auth-github": "^1.1.1", - "remix-typedjson": "^0.1.3", + "remix-typedjson": "~0.1.3", "remix-utils": "^3.4.0", "slug": "^6.0.0", "tiny-invariant": "^1.2.0", @@ -186,4 +186,4 @@ "engines": { "node": ">=16.0.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36d67f12f19..72d809741d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,7 +164,7 @@ importers: remix-auth: ^3.2.2 remix-auth-email-link: ^1.4.2 remix-auth-github: ^1.1.1 - remix-typedjson: ^0.1.3 + remix-typedjson: ~0.1.3 remix-utils: ^3.4.0 slug: ^6.0.0 start-server-and-test: ^1.14.0 From 4adad0041ae0fee6bc61d59109e61346788c37fb Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 12:28:58 -0800 Subject: [PATCH 13/71] Fixed typo --- apps/docs/integrations/apis/slack/actions/post-message.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index 3310f1fbb4b..64203e10a01 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -33,7 +33,7 @@ Publish slack messages to a public or private channel in your Slack Workspace as You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#) to see it in action. - Full full detail on block see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). + Full full details on Slack blocks see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). From 8372cf71d4304ad016ed83b8a62d46055f76eb72 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 12:51:07 -0800 Subject: [PATCH 14/71] Fixed a dead link in the docs --- apps/docs/integrations/apis/slack/actions/post-message.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index 64203e10a01..ce4cd6fe66f 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -31,7 +31,7 @@ Publish slack messages to a public or private channel in your Slack Workspace as You can use blocks to create a rich message with images, buttons, and more. - You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#) to see it in action. + You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#blocks-with-interactivity) to see it in action. Full full details on Slack blocks see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). From f660810fb880171cc16d6867ba6f739f5f803b5c Mon Sep 17 00:00:00 2001 From: D-K-P Date: Sun, 29 Jan 2023 12:52:01 -0800 Subject: [PATCH 15/71] Added 2 more integration logos --- apps/webapp/public/integrations/plausible.png | Bin 0 -> 24274 bytes apps/webapp/public/integrations/supabase.png | Bin 0 -> 13957 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/webapp/public/integrations/plausible.png create mode 100644 apps/webapp/public/integrations/supabase.png diff --git a/apps/webapp/public/integrations/plausible.png b/apps/webapp/public/integrations/plausible.png new file mode 100644 index 0000000000000000000000000000000000000000..f71d96a64991e0d92abf520eeb832007314e9bf8 GIT binary patch literal 24274 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 FV1nDKx2XUC literal 0 HcmV?d00001 diff --git a/apps/webapp/public/integrations/supabase.png b/apps/webapp/public/integrations/supabase.png new file mode 100644 index 0000000000000000000000000000000000000000..d15273a43b13c3b734bf33fecd41da6e6f09e915 GIT binary patch literal 13957 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 literal 0 HcmV?d00001 From 36cdf50c7c528c70ebb55d6b5087b6358b03428f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 12:59:20 -0800 Subject: [PATCH 16/71] Removed dead link --- apps/docs/examples/shopify.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/docs/examples/shopify.mdx b/apps/docs/examples/shopify.mdx index c80bafd6e27..cebd371e761 100644 --- a/apps/docs/examples/shopify.mdx +++ b/apps/docs/examples/shopify.mdx @@ -4,8 +4,6 @@ sidebarTitle: "Shopify" description: "Here are some example workflows you could create using the Shopify integration." --- -You can explore all the supported Shopify functions [here](/). - ## Create a new product Integrations required: Shopify From 0141643b6681d03e0c2fca4cf243d68d88892b61 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 12:59:29 -0800 Subject: [PATCH 17/71] Updated the github docs example --- apps/docs/examples/github.mdx | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/apps/docs/examples/github.mdx b/apps/docs/examples/github.mdx index ac0242c1af8..440d6f9cdad 100644 --- a/apps/docs/examples/github.mdx +++ b/apps/docs/examples/github.mdx @@ -4,32 +4,30 @@ sidebarTitle: "GitHub" description: "Here are some example workflows you could create using the GitHub integration." --- -You can explore all the supported GitHub functions [here](/integrations/apis/github/events/push). - ## When a GitHub issue is created or modified, post to Slack Integrations required: GitHub, Slack ```ts -import { Trigger, customEvent } from "@trigger.dev/sdk"; import { github, slack } from "@trigger.dev/integrations"; -import { z } from "zod"; +import { Trigger } from "@trigger.dev/sdk"; new Trigger({ - id: "my-workflow-1", - name: "Posts to Slack when GitHub Issue created or modified", - apiKey: "", - on: github.events.issueEvent({ - repo: "my-github-org/my-github-repo", + id: "new-github-star-to-slack", + name: "New GitHub Star", + apiKey: "", + logLevel: "debug", + on: github.events.newStarEvent({ + repo: "triggerdotdev/trigger.dev", }), - run: async (event, ctx) => { - const response = await slack.postMessage("send-to-slack", { - channelName: "my-slack-channel-name", - text: `A new issue has been created or modified. ${event.action}`, + run: async (event) => { + await slack.postMessage("github-stars", { + channelName: "github-stars", + text: `New GitHub star from \n<${event.sender.html_url}|${event.sender.login}>`, }); - - return response.message; }, }).listen(); ``` + +You can explore all the supported GitHub functions [here](/integrations/apis/github/events/push). From b129381cf265b8be68354785892833cc2e6e1a32 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 12:59:53 -0800 Subject: [PATCH 18/71] Improved docs title for github star example --- apps/docs/integrations/apis/slack/actions/post-message.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index ce4cd6fe66f..58ec7566efa 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -77,11 +77,11 @@ Publish slack messages to a public or private channel in your Slack Workspace as ## Example Workflows -### Notify Slack on New Star +### Notify Slack on New GitHub Star -```typescript Notify Slack on New Star +```typescript Notify Slack on New GitHub Star import { slack, github } from "@trigger.dev/integrations"; new Trigger({ From b8dc408f0f2a78277a50975d5d4ad8f77745764c Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 13:03:29 -0800 Subject: [PATCH 19/71] Title improvement --- apps/docs/integrations/apis/github/events/new-star.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/integrations/apis/github/events/new-star.mdx b/apps/docs/integrations/apis/github/events/new-star.mdx index db772559a8b..7fe272a17de 100644 --- a/apps/docs/integrations/apis/github/events/new-star.mdx +++ b/apps/docs/integrations/apis/github/events/new-star.mdx @@ -50,7 +50,7 @@ new Trigger({ -```typescript Notify Slack on New Star +```typescript Notify Slack on New GitHub Star import { slack } from "@trigger.dev/integrations"; new Trigger({ From c1ca53f513c99db70d12c2ac8518d3195c9cc32f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 13:05:52 -0800 Subject: [PATCH 20/71] Typo --- apps/docs/integrations/apis/slack/actions/post-message.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index 58ec7566efa..6ccae88d6db 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -4,7 +4,7 @@ sidebarTitle: "Post Message" description: "Post a message to a Slack channel" --- -Publish slack messages to a public or private channel in your Slack Workspace as the Trigger.dev Slack bot. If you need to publish messages to your customer's Slack channels, consider using [Incoming Webhooks](https://api.slack.com/messaging/webhooks) and our [fetch](/functions/fetch) function. +Publish Slack messages to a public or private channel in your Slack Workspace as the Trigger.dev Slack bot. If you need to publish messages to your customer's Slack channels, consider using [Incoming Webhooks](https://api.slack.com/messaging/webhooks) and our [fetch](/functions/fetch) function. ## Params From 23fc242dde3f20c69b79f54b13ac2b6635367688 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 13:09:09 -0800 Subject: [PATCH 21/71] Typo --- apps/docs/integrations/apis/slack/actions/post-message.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index 6ccae88d6db..a7f446afba5 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -31,9 +31,9 @@ Publish Slack messages to a public or private channel in your Slack Workspace as You can use blocks to create a rich message with images, buttons, and more. - You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#blocks-with-interactivity) to see it in action. + You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack as it's much easier to read and write. See the [block example](#blocks-with-interactivity) to see it in action. - Full full details on Slack blocks see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). + For full details on Slack blocks, see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks). From c96f9f10b4980a32e3e483d08a9043b18a21385a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 29 Jan 2023 21:10:34 +0000 Subject: [PATCH 22/71] Rename Reference to SDK Reference --- apps/docs/mint.json | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 77f128cced6..c4a42f46cc0 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -49,7 +49,11 @@ "navigation": [ { "group": "Getting Started", - "pages": ["welcome", "getting-started", "get-help"] + "pages": [ + "welcome", + "getting-started", + "get-help" + ] }, { "group": "Examples", @@ -77,11 +81,15 @@ "pages": [ { "group": "Slack", - "pages": ["integrations/apis/slack/actions/post-message"] + "pages": [ + "integrations/apis/slack/actions/post-message" + ] }, { "group": "Resend.com", - "pages": ["integrations/apis/resend/actions/send-email"] + "pages": [ + "integrations/apis/resend/actions/send-email" + ] } ] }, @@ -108,7 +116,7 @@ ] }, { - "group": "Reference", + "group": "SDK Reference", "pages": [ "reference/trigger", "reference/custom-event", @@ -140,4 +148,4 @@ "github": "https://github.com/triggerdotdev/trigger.dev", "discord": "https://discord.gg/nkqV9xBYWy" } -} +} \ No newline at end of file From 6c3c99123bb9b4a79d53d19328872b051934da28 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 29 Jan 2023 22:23:38 +0000 Subject: [PATCH 23/71] Make it easier to start ngrok in different regions --- scripts/proxy-pizzly.sh | 2 +- scripts/proxy-webapp.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/proxy-pizzly.sh b/scripts/proxy-pizzly.sh index 52a53676fed..be078b73388 100755 --- a/scripts/proxy-pizzly.sh +++ b/scripts/proxy-pizzly.sh @@ -20,4 +20,4 @@ set -e -x -ngrok http --subdomain=${1:-$NGROK_SUBDOMAIN} --region eu 3004 +ngrok http --subdomain=${1:-$NGROK_SUBDOMAIN} --region ${2:-eu} 3004 diff --git a/scripts/proxy-webapp.sh b/scripts/proxy-webapp.sh index 80bea6db2d6..77e739f6139 100755 --- a/scripts/proxy-webapp.sh +++ b/scripts/proxy-webapp.sh @@ -20,4 +20,4 @@ set -e -x -ngrok http --subdomain=${1:-$NGROK_SUBDOMAIN} --region eu 3000 +ngrok http --subdomain=${1:-$NGROK_SUBDOMAIN} --region ${2:-eu} 3000 From a015e535f0b9e4a99e1ebdb1cb0e5b9d156d9af8 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Sun, 29 Jan 2023 16:02:48 -0800 Subject: [PATCH 24/71] Added github star example to the docs --- apps/docs/examples/examples.mdx | 2 +- apps/docs/examples/github.mdx | 54 ++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/docs/examples/examples.mdx b/apps/docs/examples/examples.mdx index e1addc36d8c..c1bd47e1383 100644 --- a/apps/docs/examples/examples.mdx +++ b/apps/docs/examples/examples.mdx @@ -9,7 +9,7 @@ You can use these workflows in your product by following the instructions on eac - When a GitHub issue is created or modified, post a message to Slack. + When a GitHub repo is starred, post information about the user to Slack Create a new product in my Shopify store. diff --git a/apps/docs/examples/github.mdx b/apps/docs/examples/github.mdx index 440d6f9cdad..d89e2af705b 100644 --- a/apps/docs/examples/github.mdx +++ b/apps/docs/examples/github.mdx @@ -4,6 +4,37 @@ sidebarTitle: "GitHub" description: "Here are some example workflows you could create using the GitHub integration." --- +You can explore all the supported GitHub functions [here](/integrations/apis/github/events/push). + +## When a GitHub repo is starred, post information about the user to Slack + +Integrations required: GitHub, Slack + + +```ts +import { github, slack } from "@trigger.dev/integrations"; +import { Trigger } from "@trigger.dev/sdk"; +import dotenv from "dotenv"; +dotenv.config(); + +new Trigger({ +id: "new-github-star-to-slack", +name: "New GitHub Star: triggerdotdev/trigger.dev", +apiKey: "", +logLevel: "debug", +on: github.events.newStarEvent({ +repo: "triggerdotdev/trigger.dev", +}), + +run: async (event) => { +await slack.postMessage("github-stars", { +channelName: "github-stars", +text: `New GitHub star from \n<${event.sender.html_url}|${event.sender.login}>`, +}); +}, +}).listen(); +``` + ## When a GitHub issue is created or modified, post to Slack Integrations required: GitHub, Slack @@ -13,20 +44,21 @@ import { github, slack } from "@trigger.dev/integrations"; import { Trigger } from "@trigger.dev/sdk"; new Trigger({ - id: "new-github-star-to-slack", - name: "New GitHub Star", - apiKey: "", - logLevel: "debug", - on: github.events.newStarEvent({ - repo: "triggerdotdev/trigger.dev", + id: "my-workflow-1", + name: "Posts to Slack when GitHub Issue created or modified", + apiKey: "", + on: github.events.issueEvent({ + repo: "my-github-org/my-github-repo" }), - run: async (event) => { - await slack.postMessage("github-stars", { - channelName: "github-stars", - text: `New GitHub star from \n<${event.sender.html_url}|${event.sender.login}>`, + run: async (event, ctx) => { + const response = await slack.postMessage("send-to-slack", { + channelName: "my-slack-channel-name", + text: `A new issue has been created or modified. ${event.action}` }); - }, + + return response.message; + } }).listen(); ``` From e2261942a04ead26999deceeb7bf239da538f2bd Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 09:29:46 +0000 Subject: [PATCH 25/71] Added openapi spec doc and a reference page for the POST /events endpoint --- apps/docs/api/events/sendEvent.mdx | 6 ++ apps/docs/mint.json | 11 +++ apps/docs/openapi-spec.yaml | 101 +++++++++++++++++++++++- apps/webapp/app/routes/api/v1/events.ts | 6 +- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 apps/docs/api/events/sendEvent.mdx diff --git a/apps/docs/api/events/sendEvent.mdx b/apps/docs/api/events/sendEvent.mdx new file mode 100644 index 00000000000..dd9836b8bf2 --- /dev/null +++ b/apps/docs/api/events/sendEvent.mdx @@ -0,0 +1,6 @@ +--- +title: "Send Event" +openapi: "POST /events" +sidebarTitle: "Send Event" +description: "Send a custom event to Trigger through the API." +--- diff --git a/apps/docs/mint.json b/apps/docs/mint.json index c4a42f46cc0..e5c8c820d7d 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -125,6 +125,12 @@ "reference/send-event" ] }, + { + "group": "API Reference", + "pages": [ + "api/events/sendEvent" + ] + }, { "group": "Webhook Catalog", "pages": [ @@ -147,5 +153,10 @@ "twitter": "https://twitter.com/triggerdotdev", "github": "https://github.com/triggerdotdev/trigger.dev", "discord": "https://discord.gg/nkqV9xBYWy" + }, + "api": { + "auth": { + "method": "bearer" + } } } \ No newline at end of file diff --git a/apps/docs/openapi-spec.yaml b/apps/docs/openapi-spec.yaml index 8395e3ad24f..2bf1f165b72 100644 --- a/apps/docs/openapi-spec.yaml +++ b/apps/docs/openapi-spec.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Trigger.dev API - version: v1.0.0 + version: v0.2.3 description: Please see https://docs.trigger.dev for more details. contact: name: Eric @@ -11,10 +11,25 @@ info: servers: - url: https://app.trigger.dev/api/v1 description: Trigger.dev +tags: + - name: "events" + description: "Send and manage custom events" +externalDocs: + description: "Trigger.dev Docs" + url: "https://docs.trigger.dev" +security: + - apiKey: [] paths: /events: post: summary: Send Custom Event + description: Send a custom event to run your customEvent Triggers + operationId: "sendEvent" + tags: + - events + externalDocs: + description: "API method documentation" + url: "https://docs.trigger.dev/api/events/sendEvent" requestBody: required: true content: @@ -27,10 +42,92 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/customEventResponse" + $ref: "#/components/schemas/customEvent" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/error" + "405": + description: Method not allowed + content: + application/json: + schema: + $ref: "#/components/schemas/error" "422": description: Unprocessable Entity content: application/json: schema: $ref: "#/components/schemas/error" +components: + schemas: + error: + type: object + properties: + error: + type: string + customEvent: + type: object + required: + - id + - event + properties: + id: + type: string + description: "A unique event identifier, can be used to deduplicate events. Use a sufficiently unique ID generator, such as UUIDv4" + event: + type: object + additionalProperties: false + required: + - name + - payload + properties: + name: + type: string + example: user.created + description: "The name of the event. This is the same value you'd use in your customEvent trigger code to listen for events" + payload: + type: object + additionalProperties: true + description: "The JSON payload of the event" + context: + type: object + additionalProperties: true + description: "An optional context object for the event. This can be used to pass additional information about the event, such as the user who triggered it. Note that this is not currently exposed to the workflow (coming soon)." + timestamp: + type: string + format: "date-time" + example: "2022-01-01T12:30:00Z" + description: "An optional timestamp for the event. If not provided, one will be generated. Must be in ISO 8601 format (e.g. new Date().toISOString())" + delay: + type: object + description: "An optional delay object for the event. This can be used to delay the event by a certain amount of time. " + oneOf: + - type: object + properties: + seconds: + type: integer + - type: object + properties: + minutes: + type: integer + - type: object + properties: + hours: + type: integer + - type: object + properties: + days: + type: integer + - type: object + properties: + until: + type: string + format: date-time + securitySchemes: + apiKey: + type: apiKey + in: header + name: Authorization \ No newline at end of file diff --git a/apps/webapp/app/routes/api/v1/events.ts b/apps/webapp/app/routes/api/v1/events.ts index 7f1ac0c42ce..8f0202f036f 100644 --- a/apps/webapp/app/routes/api/v1/events.ts +++ b/apps/webapp/app/routes/api/v1/events.ts @@ -4,6 +4,7 @@ import { CustomEventSchema } from "@trigger.dev/common-schemas"; import { z } from "zod"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server"; +import { generateErrorMessage } from "zod-error"; const EventBodySchema = z.object({ id: z.string(), @@ -29,7 +30,10 @@ export async function action({ request }: ActionArgs) { const eventBody = EventBodySchema.safeParse(body); if (!eventBody.success) { - return json({ error: eventBody.error.message }, { status: 400 }); + return json( + { error: generateErrorMessage(eventBody.error.issues) }, + { status: 422 } + ); } const service = new IngestCustomEvent(); From 3f9e7f4bdae76e4335f23b1fb2fc405f7f7983ff Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 09:31:21 +0000 Subject: [PATCH 26/71] Cleanup the github stars example --- apps/docs/examples/github.mdx | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/apps/docs/examples/github.mdx b/apps/docs/examples/github.mdx index d89e2af705b..12c61a0d95a 100644 --- a/apps/docs/examples/github.mdx +++ b/apps/docs/examples/github.mdx @@ -10,28 +10,23 @@ You can explore all the supported GitHub functions [here](/integrations/apis/git Integrations required: GitHub, Slack - ```ts import { github, slack } from "@trigger.dev/integrations"; import { Trigger } from "@trigger.dev/sdk"; -import dotenv from "dotenv"; -dotenv.config(); new Trigger({ -id: "new-github-star-to-slack", -name: "New GitHub Star: triggerdotdev/trigger.dev", -apiKey: "", -logLevel: "debug", -on: github.events.newStarEvent({ -repo: "triggerdotdev/trigger.dev", -}), - -run: async (event) => { -await slack.postMessage("github-stars", { -channelName: "github-stars", -text: `New GitHub star from \n<${event.sender.html_url}|${event.sender.login}>`, -}); -}, + id: "new-github-star-to-slack", + name: "New GitHub Star: triggerdotdev/trigger.dev", + apiKey: "", + on: github.events.newStarEvent({ + repo: "triggerdotdev/trigger.dev", + }), + run: async (event) => { + await slack.postMessage("github-stars", { + channelName: "github-stars", + text: `New GitHub star from \n<${event.sender.html_url}|${event.sender.login}>`, + }); + }, }).listen(); ``` @@ -48,17 +43,17 @@ new Trigger({ name: "Posts to Slack when GitHub Issue created or modified", apiKey: "", on: github.events.issueEvent({ - repo: "my-github-org/my-github-repo" + repo: "my-github-org/my-github-repo", }), run: async (event, ctx) => { const response = await slack.postMessage("send-to-slack", { channelName: "my-slack-channel-name", - text: `A new issue has been created or modified. ${event.action}` + text: `A new issue has been created or modified. ${event.action}`, }); return response.message; - } + }, }).listen(); ``` From d71c61fc56fd9d5eb9f7ab46e52c30617bf30696 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 12:57:37 +0000 Subject: [PATCH 27/71] =?UTF-8?q?Break=20out=20github=20into=20it=E2=80=99?= =?UTF-8?q?s=20own=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/workflowListPresenter.server.ts | 4 +- .../handleExternalSource.server.ts | 4 +- .../registerExternalSource.server.ts | 5 +- .../fetches/performFetchRequest.server.ts | 2 +- .../performIntegrationRequest.server.ts | 2 +- .../workflows/registerWorkflow.server.ts | 2 +- apps/webapp/package.json | 4 +- apps/webapp/remix.config.js | 3 + apps/webapp/tsconfig.json | 10 +- config-packages/tsconfig/examples.json | 21 +- examples/github-webhook/package.json | 4 +- examples/github-webhook/src/index.ts | 18 +- integrations/github/README.md | 3 + integrations/github/package.json | 39 ++ integrations/github/src/events.ts | 239 +++++++++ integrations/github/src/index.ts | 3 + integrations/github/src/internal.ts | 16 + .../github/src/schemas/commitComments.ts | 31 ++ integrations/github/src/schemas/index.ts | 39 ++ .../github/src/schemas/issueComments.ts | 91 ++++ integrations/github/src/schemas/issues.ts | 311 +++++++++++ .../github/src/schemas/pullRequestComments.ts | 285 ++++++++++ .../github/src/schemas/pullRequestReviews.ts | 79 +++ .../github/src/schemas/pullRequests.ts | 401 ++++++++++++++ integrations/github/src/schemas/pushes.ts | 27 + integrations/github/src/schemas/shared.ts | 507 ++++++++++++++++++ integrations/github/src/schemas/stars.ts | 22 + integrations/github/src/types.ts | 0 integrations/github/src/webhooks.ts | 223 ++++++++ integrations/github/tsconfig.json | 20 + integrations/github/tsup.config.ts | 23 + packages/integration-sdk/README.md | 1 + packages/integration-sdk/package.json | 30 ++ packages/integration-sdk/src/index.ts | 15 + .../integration-sdk/src/services/index.ts | 137 +++++ packages/integration-sdk/src/types.ts | 91 ++++ packages/integration-sdk/tsconfig.json | 10 + packages/integration-sdk/tsup.config.ts | 19 + pnpm-lock.yaml | 112 +++- pnpm-workspace.yaml | 1 + 40 files changed, 2800 insertions(+), 54 deletions(-) create mode 100644 integrations/github/README.md create mode 100644 integrations/github/package.json create mode 100644 integrations/github/src/events.ts create mode 100644 integrations/github/src/index.ts create mode 100644 integrations/github/src/internal.ts create mode 100644 integrations/github/src/schemas/commitComments.ts create mode 100644 integrations/github/src/schemas/index.ts create mode 100644 integrations/github/src/schemas/issueComments.ts create mode 100644 integrations/github/src/schemas/issues.ts create mode 100644 integrations/github/src/schemas/pullRequestComments.ts create mode 100644 integrations/github/src/schemas/pullRequestReviews.ts create mode 100644 integrations/github/src/schemas/pullRequests.ts create mode 100644 integrations/github/src/schemas/pushes.ts create mode 100644 integrations/github/src/schemas/shared.ts create mode 100644 integrations/github/src/schemas/stars.ts create mode 100644 integrations/github/src/types.ts create mode 100644 integrations/github/src/webhooks.ts create mode 100644 integrations/github/tsconfig.json create mode 100644 integrations/github/tsup.config.ts create mode 100644 packages/integration-sdk/README.md create mode 100644 packages/integration-sdk/package.json create mode 100644 packages/integration-sdk/src/index.ts create mode 100644 packages/integration-sdk/src/services/index.ts create mode 100644 packages/integration-sdk/src/types.ts create mode 100644 packages/integration-sdk/tsconfig.json create mode 100644 packages/integration-sdk/tsup.config.ts diff --git a/apps/webapp/app/models/workflowListPresenter.server.ts b/apps/webapp/app/models/workflowListPresenter.server.ts index 956610654b7..e11d798eb42 100644 --- a/apps/webapp/app/models/workflowListPresenter.server.ts +++ b/apps/webapp/app/models/workflowListPresenter.server.ts @@ -4,8 +4,8 @@ import { SlackInteractionSourceSchema, } from "@trigger.dev/common-schemas"; import cronstrue from "cronstrue"; -import type { DisplayProperties } from "internal-integrations"; -import { github } from "internal-integrations"; +import type { DisplayProperties } from "@trigger.dev/integration-sdk"; +import * as github from "@trigger.dev/github/internal"; import invariant from "tiny-invariant"; import { triggerLabel } from "~/components/triggers/triggerLabel"; import type { PrismaClient } from "~/db.server"; diff --git a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts index 7a5de15838b..7a697f93567 100644 --- a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts +++ b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts @@ -1,8 +1,8 @@ import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; -import { github } from "internal-integrations"; +import * as github from "@trigger.dev/github/internal"; import type { ExternalSourceWithConnection } from "~/models/externalSource.server"; -import type { NormalizedRequest } from "internal-integrations"; +import type { NormalizedRequest } from "@trigger.dev/integration-sdk"; import { IngestEvent } from "../events/ingest.server"; import { ManualWebhookSourceSchema } from "@trigger.dev/common-schemas"; import { createHmac, timingSafeEqual } from "node:crypto"; diff --git a/apps/webapp/app/services/externalSources/registerExternalSource.server.ts b/apps/webapp/app/services/externalSources/registerExternalSource.server.ts index 282d940df5a..114bc71e011 100644 --- a/apps/webapp/app/services/externalSources/registerExternalSource.server.ts +++ b/apps/webapp/app/services/externalSources/registerExternalSource.server.ts @@ -1,10 +1,9 @@ import type { APIConnection, ExternalSource } from ".prisma/client"; -import type { AccessInfo } from "internal-integrations"; -import { github } from "internal-integrations"; +import type { AccessInfo } from "@trigger.dev/integration-sdk"; +import * as github from "@trigger.dev/github/internal"; import crypto from "node:crypto"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; -import { env } from "~/env.server"; import { buildExternalSourceUrl, findExternalSourceById, diff --git a/apps/webapp/app/services/fetches/performFetchRequest.server.ts b/apps/webapp/app/services/fetches/performFetchRequest.server.ts index bd780eef438..492ff00e3da 100644 --- a/apps/webapp/app/services/fetches/performFetchRequest.server.ts +++ b/apps/webapp/app/services/fetches/performFetchRequest.server.ts @@ -4,7 +4,7 @@ import { FetchRequestSchema } from "@trigger.dev/common-schemas"; import type { NormalizedResponse, PerformedRequestResponse, -} from "internal-integrations"; +} from "@trigger.dev/integration-sdk"; import type { z } from "zod"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index 0c2275dfc9b..b59b99f7501 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -3,7 +3,7 @@ import type { CacheService, NormalizedResponse, PerformedRequestResponse, -} from "internal-integrations"; +} from "@trigger.dev/integration-sdk"; import { resend, shopify, slack } from "internal-integrations"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts index 90da212f76b..0a0a8f6d8be 100644 --- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts +++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts @@ -1,4 +1,4 @@ -import { github } from "internal-integrations"; +import * as github from "@trigger.dev/github/internal"; import type { WorkflowMetadata } from "internal-platform"; import { WorkflowMetadataSchema } from "internal-platform"; import crypto from "node:crypto"; diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 8f3b9b10223..8678e1abe2c 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -64,6 +64,7 @@ "@tanstack/react-table": "^8.0.0-alpha.87", "@trigger.dev/common-schemas": "workspace:*", "@trigger.dev/providers": "workspace:*", + "@trigger.dev/github": "workspace:*", "@typeform/embed-react": "^2.14.1", "@uiw/react-codemirror": "^4.13.2", "bcryptjs": "^2.4.3", @@ -134,6 +135,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "@trigger.dev/tailwind-config": "workspace:*", + "@trigger.dev/integration-sdk": "workspace:*", "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/eslint": "^8.4.6", @@ -186,4 +188,4 @@ "engines": { "node": ">=16.0.0" } -} +} \ No newline at end of file diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index 9861e581175..1bf20b954a3 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -14,9 +14,11 @@ module.exports = { "internal-integrations", "internal-bridge", "@trigger.dev/providers", + "@trigger.dev/github", "@trigger.dev/common-schemas", "@trigger.dev/sdk", "@trigger.dev/integrations", + "@trigger.dev/integration-sdk", "emails", "internal-pulsar", ], @@ -32,6 +34,7 @@ module.exports = { "../../packages/internal-platform/src/**/*", "../../packages/trigger-sdk/src/**/*", "../../packages/trigger-integrations/src/**/*", + "../../integrations/github/src/**/*", ]; }, }; diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index eca32087416..b9d37884b90 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -43,7 +43,15 @@ "emails": ["../../packages/emails/src/index"], "emails/*": ["../../packages/emails/src/*"], "internal-pulsar": ["../../packages/internal-pulsar/src/index"], - "internal-pulsar/*": ["../../packages/internal-pulsar/src/*"] + "internal-pulsar/*": ["../../packages/internal-pulsar/src/*"], + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/github": ["../../integrations/github/src/index"], + "@trigger.dev/github/*": ["../../integrations/github/src/*"] }, "noEmit": true } diff --git a/config-packages/tsconfig/examples.json b/config-packages/tsconfig/examples.json index d150976ca74..48479fab9f9 100644 --- a/config-packages/tsconfig/examples.json +++ b/config-packages/tsconfig/examples.json @@ -11,18 +11,31 @@ "forceConsistentCasingInFileNames": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "lib": ["esnext", "dom"], + "lib": [ + "esnext", + "dom" + ], "outDir": "lib", "moduleResolution": "node", "paths": { - "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], - "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": [ + "../../packages/trigger-sdk/src/index" + ], + "@trigger.dev/sdk/*": [ + "../../packages/trigger-sdk/src/*" + ], "@trigger.dev/integrations": [ "../../packages/trigger-integrations/src/index" ], "@trigger.dev/integrations/*": [ "../../packages/trigger-integrations/src/*" + ], + "@trigger.dev/github": [ + "../../integrations/github/src/index" + ], + "@trigger.dev/github/*": [ + "../../integrations/github/src/*" ] } } -} +} \ No newline at end of file diff --git a/examples/github-webhook/package.json b/examples/github-webhook/package.json index 3f8e14f368f..1f97af126cb 100644 --- a/examples/github-webhook/package.json +++ b/examples/github-webhook/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "description": "Example trigger.dev workflow that uses the Github Webhook integration", "dependencies": { - "@trigger.dev/integrations": "workspace:*", + "@trigger.dev/github": "workspace:*", "@trigger.dev/sdk": "workspace:*", "zod": "^3.20.2" }, @@ -16,4 +16,4 @@ "scripts": { "dev": "tsx src/index.ts" } -} +} \ No newline at end of file diff --git a/examples/github-webhook/src/index.ts b/examples/github-webhook/src/index.ts index 2d6e3b899be..ae6cae9191d 100644 --- a/examples/github-webhook/src/index.ts +++ b/examples/github-webhook/src/index.ts @@ -1,5 +1,5 @@ import { Trigger } from "@trigger.dev/sdk"; -import { github } from "@trigger.dev/integrations"; +import { events } from "@trigger.dev/github"; new Trigger({ id: "github-webhook-9", @@ -7,7 +7,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.issueEvent({ + on: events.issueEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -23,7 +23,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.issueCommentEvent({ + on: events.issueCommentEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -39,7 +39,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.pullRequestEvent({ + on: events.pullRequestEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -55,7 +55,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.pullRequestCommentEvent({ + on: events.pullRequestCommentEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -71,7 +71,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.pullRequestReviewEvent({ + on: events.pullRequestReviewEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -87,7 +87,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.pushEvent({ + on: events.pushEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -103,7 +103,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.commitCommentEvent({ + on: events.commitCommentEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { @@ -119,7 +119,7 @@ new Trigger({ apiKey: "trigger_dev_zC25mKNn6c0q", endpoint: "ws://localhost:8889/ws", logLevel: "debug", - on: github.events.newStarEvent({ + on: events.newStarEvent({ repo: "triggerdotdev/trigger.dev-examples", }), run: async (event, ctx) => { diff --git a/integrations/github/README.md b/integrations/github/README.md new file mode 100644 index 00000000000..1ad0e149738 --- /dev/null +++ b/integrations/github/README.md @@ -0,0 +1,3 @@ +# Trigger.dev GitHub integration + +View more documentation [here](https://docs.trigger.dev) diff --git a/integrations/github/package.json b/integrations/github/package.json new file mode 100644 index 00000000000..d2c15196ec9 --- /dev/null +++ b/integrations/github/package.json @@ -0,0 +1,39 @@ +{ + "name": "@trigger.dev/github", + "version": "0.1.16", + "description": "The official GitHub integration for Trigger.dev", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/tsconfig": "workspace:*", + "@trigger.dev/integration-sdk": "workspace:*", + "@types/node": "16", + "rimraf": "^3.0.2", + "tsup": "^6.5.0" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:*" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "dependencies": { + "@octokit/webhooks": "^10.4.0", + "zod": "^3.20.2" + } +} \ No newline at end of file diff --git a/integrations/github/src/events.ts b/integrations/github/src/events.ts new file mode 100644 index 00000000000..392b21823cc --- /dev/null +++ b/integrations/github/src/events.ts @@ -0,0 +1,239 @@ +import type { TriggerEvent } from "@trigger.dev/sdk"; +import * as schemas from "./schemas"; + +export function commitCommentEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "commit_comment", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["commit_comment"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["commit_comment"], + }), + manualRegistration: false, + }, + schema: schemas.commitComments.commitCommentEventSchema, + }; +} + +export function issueEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "issues", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["issues"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["issues"], + }), + manualRegistration: false, + }, + schema: schemas.issues.issuesEventSchema, + }; +} + +export function issueCommentEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "issue_comment", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["issue_comment"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["issue_comment"], + }), + manualRegistration: false, + }, + schema: schemas.issuesComments.issueCommentEventSchema, + }; +} + +export function pullRequestEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "pull_request", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["pull_request"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["pull_request"], + }), + manualRegistration: false, + }, + schema: schemas.pullRequest.pullRequestEventSchema, + }; +} + +export function pullRequestCommentEvent(params: { + repo: string; +}): TriggerEvent< + typeof schemas.pullRequestComments.pullRequestReviewCommentEventSchema +> { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "pull_request_review_comment", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["pull_request_review_comment"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["pull_request_review_comment"], + }), + manualRegistration: false, + }, + schema: schemas.pullRequestComments.pullRequestReviewCommentEventSchema, + }; +} + +export function pullRequestReviewEvent(params: { + repo: string; +}): TriggerEvent< + typeof schemas.pullRequestReviews.pullRequestReviewEventSchema +> { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "pull_request_review", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["pull_request_review"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["pull_request_review"], + }), + manualRegistration: false, + }, + schema: schemas.pullRequestReviews.pullRequestReviewEventSchema, + }; +} + +export function pushEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "push", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + }, + event: ["push"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["push"], + }), + manualRegistration: false, + }, + schema: schemas.push.pushEventSchema, + }; +} + +export function newStarEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "github", + name: "star", + filter: { + service: ["github"], + payload: { + repository: { + full_name: [params.repo], + }, + action: ["created"], + }, + event: ["star"], + }, + source: schemas.WebhookSourceSchema.parse({ + subresource: "repository", + scopes: ["repo"], + repo: params.repo, + events: ["star"], + }), + manualRegistration: false, + }, + schema: schemas.stars.starCreatedEventSchema, + }; +} diff --git a/integrations/github/src/index.ts b/integrations/github/src/index.ts new file mode 100644 index 00000000000..324692d48bf --- /dev/null +++ b/integrations/github/src/index.ts @@ -0,0 +1,3 @@ +import * as events from "./events"; + +export { events }; diff --git a/integrations/github/src/internal.ts b/integrations/github/src/internal.ts new file mode 100644 index 00000000000..02e61a23350 --- /dev/null +++ b/integrations/github/src/internal.ts @@ -0,0 +1,16 @@ +import { GitHubWebhookIntegration } from "./webhooks"; +import * as schemas from "./schemas"; + +export const provider = { + name: "GitHub", + slug: "github", + icon: "/integrations/github.png", + enabledFor: "all", + authentication: { + type: "oauth", + scopes: ["repo"], + }, + schemas, +}; + +export const webhooks = new GitHubWebhookIntegration(); diff --git a/integrations/github/src/schemas/commitComments.ts b/integrations/github/src/schemas/commitComments.ts new file mode 100644 index 00000000000..6559778b445 --- /dev/null +++ b/integrations/github/src/schemas/commitComments.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { + userSchema, + authorAssociationSchema, + repositorySchema, + installationLiteSchema, + organizationSchema, +} from "./shared"; + +export const commitCommentEventSchema = z.object({ + action: z.literal("created"), + comment: z.object({ + url: z.string(), + html_url: z.string(), + id: z.number(), + node_id: z.string(), + user: userSchema, + position: z.number().nullable(), + line: z.number().nullable(), + path: z.string().nullable(), + commit_id: z.string(), + created_at: z.string(), + updated_at: z.string(), + author_association: authorAssociationSchema, + body: z.string(), + }), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); diff --git a/integrations/github/src/schemas/index.ts b/integrations/github/src/schemas/index.ts new file mode 100644 index 00000000000..081e78e720f --- /dev/null +++ b/integrations/github/src/schemas/index.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; +import * as commitComments from "./commitComments"; +import * as issuesComments from "./issueComments"; +import * as issues from "./issues"; +import * as pullRequestComments from "./pullRequestComments"; +import * as pullRequestReviews from "./pullRequestReviews"; +import * as pullRequest from "./pullRequests"; +import * as push from "./pushes"; +import * as stars from "./stars"; + +export const WebhookRepoSourceSchema = z.object({ + subresource: z.literal("repository"), + scopes: z.array(z.string()), + repo: z.string(), + events: z.array(z.string()), +}); + +export const WebhookOrganizationSourceSchema = z.object({ + subresource: z.literal("organization"), + scopes: z.array(z.string()), + org: z.string(), + events: z.array(z.string()), +}); + +export const WebhookSourceSchema = z.union([ + WebhookRepoSourceSchema, + WebhookOrganizationSourceSchema, +]); + +export { + commitComments, + issues, + issuesComments, + pullRequest, + pullRequestComments, + pullRequestReviews, + push, + stars, +}; diff --git a/integrations/github/src/schemas/issueComments.ts b/integrations/github/src/schemas/issueComments.ts new file mode 100644 index 00000000000..8be801aad3b --- /dev/null +++ b/integrations/github/src/schemas/issueComments.ts @@ -0,0 +1,91 @@ +import { z } from "zod"; +import { issueSchema } from "./issues"; +import { + appSchema, + authorAssociationSchema, + installationLiteSchema, + labelSchema, + organizationSchema, + reactionsSchema, + repositorySchema, + userSchema, +} from "./shared"; + +export const issueCommentSchema = z.object({ + url: z.string(), + html_url: z.string(), + issue_url: z.string(), + id: z.number(), + node_id: z.string(), + user: userSchema, + created_at: z.string(), + updated_at: z.string(), + author_association: authorAssociationSchema, + body: z.string(), + reactions: reactionsSchema, + performed_via_github_app: appSchema.nullable(), +}); + +export const issueCommentCreatedEventSchema = z.object({ + action: z.literal("created"), + issue: issueSchema.and( + z.object({ + assignee: userSchema.nullable(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + labels: z.array(labelSchema), + }) + ), + comment: issueCommentSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issueCommentDeletedEventSchema = z.object({ + action: z.literal("deleted"), + issue: issueSchema.and( + z.object({ + assignee: userSchema.nullable(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + labels: z.array(labelSchema), + }) + ), + comment: issueCommentSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issueCommentEditedEventSchema = z.object({ + action: z.literal("edited"), + changes: z.object({ + body: z + .object({ + from: z.string(), + }) + .optional(), + }), + issue: issueSchema.and( + z.object({ + assignee: userSchema.nullable(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + labels: z.array(labelSchema), + }) + ), + comment: issueCommentSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issueCommentEventSchema = z.discriminatedUnion("action", [ + issueCommentCreatedEventSchema, + issueCommentDeletedEventSchema, + issueCommentEditedEventSchema, +]); diff --git a/integrations/github/src/schemas/issues.ts b/integrations/github/src/schemas/issues.ts new file mode 100644 index 00000000000..224db1a8238 --- /dev/null +++ b/integrations/github/src/schemas/issues.ts @@ -0,0 +1,311 @@ +import { z } from "zod"; +import { + appSchema, + authorAssociationSchema, + installationLiteSchema, + labelSchema, + milestoneSchema, + organizationSchema, + reactionsSchema, + repositorySchema, + userSchema, +} from "./shared"; + +export const issueSchema = z.object({ + url: z.string(), + repository_url: z.string(), + labels_url: z.string(), + comments_url: z.string(), + events_url: z.string(), + html_url: z.string(), + id: z.number(), + node_id: z.string(), + number: z.number(), + title: z.string(), + user: userSchema, + labels: z.array(labelSchema).optional(), + state: z.union([z.literal("open"), z.literal("closed")]).optional(), + locked: z.boolean().optional(), + assignee: userSchema.optional().nullable(), + assignees: z.array(userSchema), + milestone: milestoneSchema.nullable(), + comments: z.number(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + author_association: authorAssociationSchema, + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), + draft: z.boolean().optional(), + performed_via_github_app: appSchema.optional().nullable(), + pull_request: z + .object({ + url: z.string().optional(), + html_url: z.string().optional(), + diff_url: z.string().optional(), + patch_url: z.string().optional(), + merged_at: z.string().optional().nullable(), + }) + .optional(), + body: z.string().nullable(), + reactions: reactionsSchema, + timeline_url: z.string().optional(), + state_reason: z.string().optional().nullable(), +}); + +export const issuesAssignedEventSchema = z.object({ + action: z.literal("assigned"), + issue: issueSchema, + assignee: userSchema.optional().nullable(), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesClosedEventSchema = z.object({ + action: z.literal("closed"), + issue: issueSchema.and( + z.object({ + state: z.literal("closed"), + closed_at: z.string(), + }) + ), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesDeletedEventSchema = z.object({ + action: z.literal("deleted"), + issue: issueSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesDemilestonedEventSchema = z.object({ + action: z.literal("demilestoned"), + issue: issueSchema.and( + z.object({ + milestone: z.null(), + }) + ), + milestone: milestoneSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesEditedEventSchema = z.object({ + action: z.literal("edited"), + issue: issueSchema, + label: labelSchema.optional(), + changes: z.object({ + body: z + .object({ + from: z.string(), + }) + .optional(), + title: z + .object({ + from: z.string(), + }) + .optional(), + }), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesLabeledEventSchema = z.object({ + action: z.literal("labeled"), + issue: issueSchema, + label: labelSchema.optional(), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesLockedEventSchema = z.object({ + action: z.literal("locked"), + issue: issueSchema.and( + z.object({ + locked: z.literal(true), + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), + }) + ), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesMilestonedEventSchema = z.object({ + action: z.literal("milestoned"), + issue: issueSchema.and( + z.object({ + milestone: milestoneSchema, + }) + ), + milestone: milestoneSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesOpenedEventSchema = z.object({ + action: z.literal("opened"), + changes: z + .object({ + old_issue: issueSchema, + old_repository: repositorySchema, + }) + .optional(), + issue: issueSchema.and( + z.object({ + state: z.literal("open"), + closed_at: z.null(), + }) + ), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesPinnedEventSchema = z.object({ + action: z.literal("pinned"), + issue: issueSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesReopenedEventSchema = z.object({ + action: z.literal("reopened"), + issue: issueSchema.and( + z.object({ + state: z.literal("open"), + }) + ), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesTransferredEventSchema = z.object({ + action: z.literal("transferred"), + changes: z.object({ + new_issue: issueSchema, + new_repository: repositorySchema, + }), + issue: issueSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesUnassignedEventSchema = z.object({ + action: z.literal("unassigned"), + issue: issueSchema, + assignee: userSchema.optional().nullable(), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesUnlabeledEventSchema = z.object({ + action: z.literal("unlabeled"), + issue: issueSchema, + label: labelSchema.optional(), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesUnlockedEventSchema = z.object({ + action: z.literal("unlocked"), + issue: issueSchema.and( + z.object({ + locked: z.literal(false), + active_lock_reason: z.null(), + }) + ), + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesUnpinnedEventSchema = z.object({ + action: z.literal("unpinned"), + issue: issueSchema, + repository: repositorySchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); + +export const issuesEventSchema: z.ZodDiscriminatedUnion< + "action", + [ + typeof issuesAssignedEventSchema, + typeof issuesClosedEventSchema, + typeof issuesDeletedEventSchema, + typeof issuesDemilestonedEventSchema, + typeof issuesEditedEventSchema, + typeof issuesLabeledEventSchema, + typeof issuesLockedEventSchema, + typeof issuesMilestonedEventSchema, + typeof issuesOpenedEventSchema, + typeof issuesPinnedEventSchema, + typeof issuesReopenedEventSchema, + typeof issuesTransferredEventSchema, + typeof issuesUnassignedEventSchema, + typeof issuesUnlabeledEventSchema, + typeof issuesUnlockedEventSchema, + typeof issuesUnpinnedEventSchema + ] +> = z.discriminatedUnion("action", [ + issuesAssignedEventSchema, + issuesClosedEventSchema, + issuesDeletedEventSchema, + issuesDemilestonedEventSchema, + issuesEditedEventSchema, + issuesLabeledEventSchema, + issuesLockedEventSchema, + issuesMilestonedEventSchema, + issuesOpenedEventSchema, + issuesPinnedEventSchema, + issuesReopenedEventSchema, + issuesTransferredEventSchema, + issuesUnassignedEventSchema, + issuesUnlabeledEventSchema, + issuesUnlockedEventSchema, + issuesUnpinnedEventSchema, +]); diff --git a/integrations/github/src/schemas/pullRequestComments.ts b/integrations/github/src/schemas/pullRequestComments.ts new file mode 100644 index 00000000000..f386dd2f2b4 --- /dev/null +++ b/integrations/github/src/schemas/pullRequestComments.ts @@ -0,0 +1,285 @@ +import { z } from "zod"; +import { + userSchema, + teamSchema, + labelSchema, + milestoneSchema, + repositorySchema, + linkSchema, + authorAssociationSchema, + installationLiteSchema, + organizationSchema, + reactionsSchema, + pullRequestAutoMergeSchema, +} from "./shared"; + +export const pullRequestReviewCommentSchema = z.object({ + url: z.string(), + pull_request_review_id: z.number(), + id: z.number(), + node_id: z.string(), + diff_hunk: z.string(), + path: z.string(), + position: z.number().nullable(), + original_position: z.number(), + commit_id: z.string(), + original_commit_id: z.string(), + user: userSchema, + body: z.string(), + created_at: z.string(), + updated_at: z.string(), + html_url: z.string(), + pull_request_url: z.string(), + author_association: authorAssociationSchema, + _links: z.object({ + self: linkSchema, + html: linkSchema, + pull_request: linkSchema, + }), + reactions: reactionsSchema, + start_line: z.number().nullable(), + original_start_line: z.number().nullable(), + start_side: z.union([z.literal("LEFT"), z.literal("RIGHT")]).nullable(), + line: z.number().nullable(), + original_line: z.number(), + side: z.union([z.literal("LEFT"), z.literal("RIGHT")]), + in_reply_to_id: z.number().optional(), +}); + +export const pullRequestReviewCommentCreatedEventSchema = z.object({ + action: z.literal("created"), + comment: pullRequestReviewCommentSchema, + pull_request: z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + html_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + issue_url: z.string(), + number: z.number(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + title: z.string(), + user: userSchema, + body: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + merged_at: z.string().nullable(), + merge_commit_sha: z.string().nullable(), + assignee: userSchema.nullable(), + assignees: z.array(userSchema), + requested_reviewers: z.array(z.union([userSchema, teamSchema])), + requested_teams: z.array(teamSchema), + labels: z.array(labelSchema), + milestone: milestoneSchema.nullable(), + draft: z.boolean().optional(), + commits_url: z.string(), + review_comments_url: z.string(), + review_comment_url: z.string(), + comments_url: z.string(), + statuses_url: z.string(), + head: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + base: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + _links: z.object({ + self: linkSchema, + html: linkSchema, + issue: linkSchema, + comments: linkSchema, + review_comments: linkSchema, + review_comment: linkSchema, + commits: linkSchema, + statuses: linkSchema, + }), + auto_merge: pullRequestAutoMergeSchema.optional().nullable(), + author_association: authorAssociationSchema, + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), + }), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewCommentDeletedEventSchema = z.object({ + action: z.literal("deleted"), + comment: pullRequestReviewCommentSchema, + pull_request: z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + html_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + issue_url: z.string(), + number: z.number(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + title: z.string(), + user: userSchema, + body: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + merged_at: z.string().nullable(), + merge_commit_sha: z.string().nullable(), + assignee: userSchema.nullable(), + assignees: z.array(userSchema), + requested_reviewers: z.array(z.union([userSchema, teamSchema])), + requested_teams: z.array(teamSchema), + labels: z.array(labelSchema), + milestone: milestoneSchema.nullable(), + draft: z.boolean().optional(), + commits_url: z.string(), + review_comments_url: z.string(), + review_comment_url: z.string(), + comments_url: z.string(), + statuses_url: z.string(), + head: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + base: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + _links: z.object({ + self: linkSchema, + html: linkSchema, + issue: linkSchema, + comments: linkSchema, + review_comments: linkSchema, + review_comment: linkSchema, + commits: linkSchema, + statuses: linkSchema, + }), + auto_merge: pullRequestAutoMergeSchema.optional().nullable(), + author_association: authorAssociationSchema, + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), + }), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewCommentEditedEventSchema = z.object({ + action: z.literal("edited"), + changes: z.object({ + body: z + .object({ + from: z.string(), + }) + .optional(), + }), + comment: pullRequestReviewCommentSchema, + pull_request: z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + html_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + issue_url: z.string(), + number: z.number(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + title: z.string(), + user: userSchema, + body: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + merged_at: z.string().nullable(), + merge_commit_sha: z.string().nullable(), + assignee: userSchema.nullable(), + assignees: z.array(userSchema), + requested_reviewers: z.array(z.union([userSchema, teamSchema])), + requested_teams: z.array(teamSchema), + labels: z.array(labelSchema), + milestone: milestoneSchema.nullable(), + draft: z.boolean().optional(), + commits_url: z.string(), + review_comments_url: z.string(), + review_comment_url: z.string(), + comments_url: z.string(), + statuses_url: z.string(), + head: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + base: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + _links: z.object({ + self: linkSchema, + html: linkSchema, + issue: linkSchema, + comments: linkSchema, + review_comments: linkSchema, + review_comment: linkSchema, + commits: linkSchema, + statuses: linkSchema, + }), + auto_merge: pullRequestAutoMergeSchema.optional().nullable(), + author_association: authorAssociationSchema, + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), + }), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewCommentEventSchema = z.union([ + pullRequestReviewCommentCreatedEventSchema, + pullRequestReviewCommentDeletedEventSchema, + pullRequestReviewCommentEditedEventSchema, +]); diff --git a/integrations/github/src/schemas/pullRequestReviews.ts b/integrations/github/src/schemas/pullRequestReviews.ts new file mode 100644 index 00000000000..378ade5b438 --- /dev/null +++ b/integrations/github/src/schemas/pullRequestReviews.ts @@ -0,0 +1,79 @@ +import { z } from "zod"; +import { + repositorySchema, + installationLiteSchema, + organizationSchema, + userSchema, + authorAssociationSchema, + linkSchema, + simplePullRequestSchema, +} from "./shared"; + +export const pullRequestReviewSchema = z.object({ + id: z.number(), + node_id: z.string(), + user: userSchema, + body: z.string().nullable(), + commit_id: z.string(), + submitted_at: z.string().nullable(), + state: z.union([ + z.literal("commented"), + z.literal("changes_requested"), + z.literal("approved"), + z.literal("dismissed"), + ]), + html_url: z.string(), + pull_request_url: z.string(), + author_association: authorAssociationSchema, + _links: z.object({ + html: linkSchema, + pull_request: linkSchema, + }), +}); + +export const pullRequestReviewDismissedEventSchema = z.object({ + action: z.literal("dismissed"), + review: pullRequestReviewSchema.and( + z.object({ + state: z.literal("dismissed"), + }) + ), + pull_request: simplePullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewEditedEventSchema = z.object({ + action: z.literal("edited"), + changes: z.object({ + body: z + .object({ + from: z.string(), + }) + .optional(), + }), + review: pullRequestReviewSchema, + pull_request: simplePullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewSubmittedEventSchema = z.object({ + action: z.literal("submitted"), + review: pullRequestReviewSchema, + pull_request: simplePullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewEventSchema = z.union([ + pullRequestReviewDismissedEventSchema, + pullRequestReviewEditedEventSchema, + pullRequestReviewSubmittedEventSchema, +]); diff --git a/integrations/github/src/schemas/pullRequests.ts b/integrations/github/src/schemas/pullRequests.ts new file mode 100644 index 00000000000..448bd754b5c --- /dev/null +++ b/integrations/github/src/schemas/pullRequests.ts @@ -0,0 +1,401 @@ +import { z } from "zod"; +import { + authorAssociationSchema, + installationLiteSchema, + labelSchema, + linkSchema, + milestoneSchema, + organizationSchema, + pullRequestAutoMergeSchema, + repositorySchema, + teamSchema, + userSchema, +} from "./shared"; + +export const pullRequestSchema = z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + html_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + issue_url: z.string(), + number: z.number(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + title: z.string(), + user: userSchema, + body: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + merged_at: z.string().nullable(), + merge_commit_sha: z.string().nullable(), + assignee: userSchema.nullable(), + assignees: z.array(userSchema), + requested_reviewers: z.array(z.union([userSchema, teamSchema])), + requested_teams: z.array(teamSchema), + labels: z.array(labelSchema), + milestone: milestoneSchema.nullable(), + commits_url: z.string(), + review_comments_url: z.string(), + review_comment_url: z.string(), + comments_url: z.string(), + statuses_url: z.string(), + head: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + base: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + _links: z.object({ + self: linkSchema, + html: linkSchema, + issue: linkSchema, + comments: linkSchema, + review_comments: linkSchema, + review_comment: linkSchema, + commits: linkSchema, + statuses: linkSchema, + }), + author_association: authorAssociationSchema, + auto_merge: pullRequestAutoMergeSchema.nullable(), + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), + draft: z.boolean(), + merged: z.boolean().nullable(), + mergeable: z.boolean().nullable(), + rebaseable: z.boolean().nullable(), + mergeable_state: z.string(), + merged_by: userSchema.nullable(), + comments: z.number(), + review_comments: z.number(), + maintainer_can_modify: z.boolean(), + commits: z.number(), + additions: z.number(), + deletions: z.number(), + changed_files: z.number(), +}); + +export const pullRequestAssignedEventSchema = z.object({ + action: z.literal("assigned"), + number: z.number(), + pull_request: pullRequestSchema, + assignee: userSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestAutoMergeDisabledEventSchema = z.object({ + action: z.literal("auto_merge_disabled"), + number: z.number(), + pull_request: pullRequestSchema, + reason: z.string(), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestAutoMergeEnabledEventSchema = z.object({ + action: z.literal("auto_merge_enabled"), + number: z.number(), + pull_request: pullRequestSchema, + reason: z.string(), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestClosedEventSchema = z.object({ + action: z.literal("closed"), + number: z.number(), + pull_request: pullRequestSchema.and( + z.object({ + state: z.literal("closed"), + closed_at: z.string(), + merged: z.boolean(), + }) + ), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestConvertedToDraftEventSchema = z.object({ + action: z.literal("converted_to_draft"), + number: z.number(), + pull_request: pullRequestSchema.and( + z.object({ + closed_at: z.null(), + merged_at: z.null(), + draft: z.literal(true), + merged: z.literal(false), + merged_by: z.null(), + }) + ), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestDequeuedEventSchema = z.object({ + action: z.literal("dequeued"), + number: z.number(), + reason: z.string(), + pull_request: pullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestEditedEventSchema = z.object({ + action: z.literal("edited"), + number: z.number(), + changes: z.object({ + body: z + .object({ + from: z.string(), + }) + .optional(), + title: z + .object({ + from: z.string(), + }) + .optional(), + base: z + .object({ + ref: z.object({ + from: z.string(), + }), + sha: z.object({ + from: z.string(), + }), + }) + .optional(), + }), + pull_request: pullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestLabeledEventSchema = z.object({ + action: z.literal("labeled"), + number: z.number(), + pull_request: pullRequestSchema, + label: labelSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestLockedEventSchema = z.object({ + action: z.literal("locked"), + number: z.number(), + pull_request: pullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestOpenedEventSchema = z.object({ + action: z.literal("opened"), + number: z.number(), + pull_request: pullRequestSchema.and( + z.object({ + state: z.literal("open"), + closed_at: z.null(), + merged_at: z.null(), + active_lock_reason: z.null(), + merged_by: z.null(), + }) + ), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestQueuedEventSchema = z.object({ + action: z.literal("queued"), + number: z.number(), + pull_request: pullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReadyForReviewEventSchema = z.object({ + action: z.literal("ready_for_review"), + number: z.number(), + pull_request: pullRequestSchema.and( + z.object({ + state: z.literal("open"), + closed_at: z.null(), + merged_at: z.null(), + draft: z.literal(false), + merged: z.boolean(), + merged_by: z.null(), + }) + ), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReopenedEventSchema = z.object({ + action: z.literal("reopened"), + number: z.number(), + pull_request: pullRequestSchema.and( + z.object({ + state: z.literal("open"), + closed_at: z.null(), + merged_at: z.null(), + merged: z.boolean(), + merged_by: z.null(), + }) + ), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestSynchronizeEventSchema = z.object({ + action: z.literal("synchronize"), + number: z.number(), + before: z.string(), + after: z.string(), + pull_request: pullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestUnassignedEventSchema = z.object({ + action: z.literal("unassigned"), + number: z.number(), + pull_request: pullRequestSchema, + assignee: userSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestUnlabeledEventSchema = z.object({ + action: z.literal("unlabeled"), + number: z.number(), + pull_request: pullRequestSchema, + label: labelSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestUnlockedEventSchema = z.object({ + action: z.literal("unlocked"), + number: z.number(), + pull_request: pullRequestSchema, + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewRequestRemovedEventSchema = z.object({ + action: z.literal("review_request_removed"), + number: z.number(), + pull_request: pullRequestSchema, + requested_reviewer: userSchema.optional(), + requested_team: teamSchema.optional(), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestReviewRequestedEventSchema = z.object({ + action: z.literal("review_requested"), + number: z.number(), + pull_request: pullRequestSchema, + requested_reviewer: userSchema.optional(), + requested_team: teamSchema.optional(), + repository: repositorySchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), + sender: userSchema, +}); + +export const pullRequestEventSchema: z.ZodDiscriminatedUnion< + "action", + [ + typeof pullRequestAssignedEventSchema, + typeof pullRequestAutoMergeDisabledEventSchema, + typeof pullRequestAutoMergeEnabledEventSchema, + typeof pullRequestClosedEventSchema, + typeof pullRequestConvertedToDraftEventSchema, + typeof pullRequestDequeuedEventSchema, + typeof pullRequestEditedEventSchema, + typeof pullRequestLabeledEventSchema, + typeof pullRequestLockedEventSchema, + typeof pullRequestOpenedEventSchema, + typeof pullRequestQueuedEventSchema, + typeof pullRequestReadyForReviewEventSchema, + typeof pullRequestReopenedEventSchema, + typeof pullRequestReviewRequestRemovedEventSchema, + typeof pullRequestReviewRequestedEventSchema, + typeof pullRequestSynchronizeEventSchema, + typeof pullRequestUnassignedEventSchema, + typeof pullRequestUnlabeledEventSchema, + typeof pullRequestUnlockedEventSchema + ] +> = z.discriminatedUnion("action", [ + pullRequestAssignedEventSchema, + pullRequestAutoMergeDisabledEventSchema, + pullRequestAutoMergeEnabledEventSchema, + pullRequestClosedEventSchema, + pullRequestConvertedToDraftEventSchema, + pullRequestDequeuedEventSchema, + pullRequestEditedEventSchema, + pullRequestLabeledEventSchema, + pullRequestLockedEventSchema, + pullRequestOpenedEventSchema, + pullRequestQueuedEventSchema, + pullRequestReadyForReviewEventSchema, + pullRequestReopenedEventSchema, + pullRequestReviewRequestRemovedEventSchema, + pullRequestReviewRequestedEventSchema, + pullRequestSynchronizeEventSchema, + pullRequestUnassignedEventSchema, + pullRequestUnlabeledEventSchema, + pullRequestUnlockedEventSchema, +]); diff --git a/integrations/github/src/schemas/pushes.ts b/integrations/github/src/schemas/pushes.ts new file mode 100644 index 00000000000..d1996500430 --- /dev/null +++ b/integrations/github/src/schemas/pushes.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; +import { + repositorySchema, + userSchema, + installationLiteSchema, + organizationSchema, + commitSchema, + committerSchema, +} from "./shared"; + +export const pushEventSchema = z.object({ + ref: z.string(), + before: z.string(), + after: z.string(), + created: z.boolean(), + deleted: z.boolean(), + forced: z.boolean(), + base_ref: z.string().nullable(), + compare: z.string(), + commits: z.array(commitSchema), + head_commit: commitSchema.nullable(), + repository: repositorySchema, + pusher: committerSchema, + sender: userSchema, + installation: installationLiteSchema.optional(), + organization: organizationSchema.optional(), +}); diff --git a/integrations/github/src/schemas/shared.ts b/integrations/github/src/schemas/shared.ts new file mode 100644 index 00000000000..d57901baa7b --- /dev/null +++ b/integrations/github/src/schemas/shared.ts @@ -0,0 +1,507 @@ +import { z } from "zod"; + +export const userSchema = z.object({ + login: z.string(), + id: z.number(), + node_id: z.string(), + name: z.string().optional(), + email: z.string().optional().nullable(), + avatar_url: z.string(), + gravatar_id: z.string(), + url: z.string(), + html_url: z.string(), + followers_url: z.string(), + following_url: z.string(), + gists_url: z.string(), + starred_url: z.string(), + subscriptions_url: z.string(), + organizations_url: z.string(), + repos_url: z.string(), + events_url: z.string(), + received_events_url: z.string(), + type: z.union([ + z.literal("Bot"), + z.literal("User"), + z.literal("Organization"), + ]), + site_admin: z.boolean(), +}); + +export const licenseSchema = z.object({ + key: z.string(), + name: z.string(), + spdx_id: z.string(), + url: z.string().nullable(), + node_id: z.string(), +}); + +export const installationLiteSchema = z.object({ + id: z.number(), + node_id: z.string(), +}); + +export const organizationSchema = z.object({ + login: z.string(), + id: z.number(), + node_id: z.string(), + url: z.string(), + html_url: z.string().optional(), + repos_url: z.string(), + events_url: z.string(), + hooks_url: z.string(), + issues_url: z.string(), + members_url: z.string(), + public_members_url: z.string(), + avatar_url: z.string(), + description: z.string().nullable(), +}); + +export const repositorySchema = z.object({ + id: z.number(), + node_id: z.string(), + name: z.string(), + full_name: z.string(), + private: z.boolean(), + owner: userSchema, + html_url: z.string(), + description: z.string().nullable(), + fork: z.boolean(), + url: z.string(), + forks_url: z.string(), + keys_url: z.string(), + collaborators_url: z.string(), + teams_url: z.string(), + hooks_url: z.string(), + issue_events_url: z.string(), + events_url: z.string(), + assignees_url: z.string(), + branches_url: z.string(), + tags_url: z.string(), + blobs_url: z.string(), + git_tags_url: z.string(), + git_refs_url: z.string(), + trees_url: z.string(), + statuses_url: z.string(), + languages_url: z.string(), + stargazers_url: z.string(), + contributors_url: z.string(), + subscribers_url: z.string(), + subscription_url: z.string(), + commits_url: z.string(), + git_commits_url: z.string(), + comments_url: z.string(), + issue_comment_url: z.string(), + contents_url: z.string(), + compare_url: z.string(), + merges_url: z.string(), + archive_url: z.string(), + downloads_url: z.string(), + issues_url: z.string(), + pulls_url: z.string(), + milestones_url: z.string(), + notifications_url: z.string(), + labels_url: z.string(), + releases_url: z.string(), + deployments_url: z.string(), + created_at: z.union([z.number(), z.string()]), + updated_at: z.string(), + pushed_at: z.union([z.number(), z.string()]).nullable(), + git_url: z.string(), + ssh_url: z.string(), + clone_url: z.string(), + svn_url: z.string(), + homepage: z.string().nullable(), + size: z.number(), + stargazers_count: z.number(), + watchers_count: z.number(), + language: z.string().nullable(), + has_issues: z.boolean(), + has_projects: z.boolean(), + has_downloads: z.boolean(), + has_wiki: z.boolean(), + has_pages: z.boolean(), + forks_count: z.number(), + mirror_url: z.string().nullable(), + archived: z.boolean(), + disabled: z.boolean().optional(), + open_issues_count: z.number(), + license: licenseSchema.nullable(), + forks: z.number(), + open_issues: z.number(), + watchers: z.number(), + stargazers: z.number().optional(), + default_branch: z.string(), + allow_squash_merge: z.boolean().optional(), + allow_merge_commit: z.boolean().optional(), + allow_rebase_merge: z.boolean().optional(), + allow_auto_merge: z.boolean().optional(), + allow_forking: z.boolean().optional(), + allow_update_branch: z.boolean().optional(), + use_squash_pr_title_as_default: z.boolean().optional(), + squash_merge_commit_message: z.string().optional(), + squash_merge_commit_title: z.string().optional(), + merge_commit_message: z.string().optional(), + merge_commit_title: z.string().optional(), + is_template: z.boolean(), + web_commit_signoff_required: z.boolean(), + topics: z.array(z.string()), + visibility: z.union([ + z.literal("public"), + z.literal("private"), + z.literal("internal"), + ]), + delete_branch_on_merge: z.boolean().optional(), + master_branch: z.string().optional(), + permissions: z + .object({ + pull: z.boolean(), + push: z.boolean(), + admin: z.boolean(), + maintain: z.boolean().optional(), + triage: z.boolean().optional(), + }) + .optional(), + public: z.boolean().optional(), + organization: z.string().optional(), +}); + +export const teamSchema = z.object({ + name: z.string(), + id: z.number(), + node_id: z.string(), + slug: z.string(), + description: z.string().nullable(), + privacy: z.union([ + z.literal("open"), + z.literal("closed"), + z.literal("secret"), + ]), + url: z.string(), + html_url: z.string(), + members_url: z.string(), + repositories_url: z.string(), + permission: z.string(), + parent: z + .object({ + name: z.string(), + id: z.number(), + node_id: z.string(), + slug: z.string(), + description: z.string().nullable(), + privacy: z.union([ + z.literal("open"), + z.literal("closed"), + z.literal("secret"), + ]), + url: z.string(), + html_url: z.string(), + members_url: z.string(), + repositories_url: z.string(), + permission: z.string(), + }) + .optional() + .nullable(), +}); + +export const linkSchema = z.object({ + href: z.string(), +}); + +export const appSchema = z.object({ + id: z.number(), + slug: z.string().optional(), + node_id: z.string(), + owner: userSchema, + name: z.string(), + description: z.string().nullable(), + external_url: z.string(), + html_url: z.string(), + created_at: z.string(), + updated_at: z.string(), + permissions: z + .object({ + actions: z.union([z.literal("read"), z.literal("write")]).optional(), + administration: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + blocking: z.union([z.literal("read"), z.literal("write")]).optional(), + checks: z.union([z.literal("read"), z.literal("write")]).optional(), + content_references: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + contents: z.union([z.literal("read"), z.literal("write")]).optional(), + deployments: z.union([z.literal("read"), z.literal("write")]).optional(), + discussions: z.union([z.literal("read"), z.literal("write")]).optional(), + emails: z.union([z.literal("read"), z.literal("write")]).optional(), + environments: z.union([z.literal("read"), z.literal("write")]).optional(), + issues: z.union([z.literal("read"), z.literal("write")]).optional(), + keys: z.union([z.literal("read"), z.literal("write")]).optional(), + members: z.union([z.literal("read"), z.literal("write")]).optional(), + merge_queues: z.union([z.literal("read"), z.literal("write")]).optional(), + metadata: z.union([z.literal("read"), z.literal("write")]).optional(), + organization_administration: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_hooks: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_packages: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_plan: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_projects: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_secrets: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_self_hosted_runners: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + organization_user_blocking: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + packages: z.union([z.literal("read"), z.literal("write")]).optional(), + pages: z.union([z.literal("read"), z.literal("write")]).optional(), + pull_requests: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + repository_hooks: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + repository_projects: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + secret_scanning_alerts: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + secrets: z.union([z.literal("read"), z.literal("write")]).optional(), + security_events: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + security_scanning_alert: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + single_file: z.union([z.literal("read"), z.literal("write")]).optional(), + statuses: z.union([z.literal("read"), z.literal("write")]).optional(), + team_discussions: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + vulnerability_alerts: z + .union([z.literal("read"), z.literal("write")]) + .optional(), + workflows: z.union([z.literal("read"), z.literal("write")]).optional(), + }) + .optional(), + events: z + .array( + z.union([ + z.literal("branch_protection_rule"), + z.literal("check_run"), + z.literal("check_suite"), + z.literal("code_scanning_alert"), + z.literal("commit_comment"), + z.literal("content_reference"), + z.literal("create"), + z.literal("delete"), + z.literal("deployment"), + z.literal("deployment_review"), + z.literal("deployment_status"), + z.literal("deploy_key"), + z.literal("discussion"), + z.literal("discussion_comment"), + z.literal("fork"), + z.literal("gollum"), + z.literal("issues"), + z.literal("issue_comment"), + z.literal("label"), + z.literal("member"), + z.literal("membership"), + z.literal("merge_group"), + z.literal("merge_queue_entry"), + z.literal("milestone"), + z.literal("organization"), + z.literal("org_block"), + z.literal("page_build"), + z.literal("project"), + z.literal("projects_v2_item"), + z.literal("project_card"), + z.literal("project_column"), + z.literal("public"), + z.literal("pull_request"), + z.literal("pull_request_review"), + z.literal("pull_request_review_comment"), + z.literal("push"), + z.literal("registry_package"), + z.literal("release"), + z.literal("repository"), + z.literal("repository_dispatch"), + z.literal("secret_scanning_alert"), + z.literal("secret_scanning_alert_location"), + z.literal("security_and_analysis"), + z.literal("star"), + z.literal("status"), + z.literal("team"), + z.literal("team_add"), + z.literal("watch"), + z.literal("workflow_dispatch"), + z.literal("workflow_run"), + z.literal("workflow_job"), + ]) + ) + .optional(), +}); + +export const reactionsSchema = z.object({ + url: z.string(), + total_count: z.number(), + "+1": z.number(), + "-1": z.number(), + laugh: z.number(), + hooray: z.number(), + confused: z.number(), + heart: z.number(), + rocket: z.number(), + eyes: z.number(), +}); + +export const authorAssociationSchema = z.union([ + z.literal("COLLABORATOR"), + z.literal("CONTRIBUTOR"), + z.literal("FIRST_TIMER"), + z.literal("FIRST_TIME_CONTRIBUTOR"), + z.literal("MANNEQUIN"), + z.literal("MEMBER"), + z.literal("NONE"), + z.literal("OWNER"), +]); + +export const milestoneSchema = z.object({ + url: z.string(), + html_url: z.string(), + labels_url: z.string(), + id: z.number(), + node_id: z.string(), + number: z.number(), + title: z.string(), + description: z.string().nullable(), + creator: userSchema, + open_issues: z.number(), + closed_issues: z.number(), + state: z.union([z.literal("open"), z.literal("closed")]), + created_at: z.string(), + updated_at: z.string(), + due_on: z.string().nullable(), + closed_at: z.string().nullable(), +}); + +export const labelSchema = z.object({ + id: z.number(), + node_id: z.string(), + url: z.string(), + name: z.string(), + description: z.string().nullable(), + color: z.string(), + default: z.boolean(), +}); + +export const pullRequestAutoMergeSchema = z.object({ + enabled_by: userSchema, + merge_method: z.union([ + z.literal("merge"), + z.literal("squash"), + z.literal("rebase"), + ]), + commit_title: z.string(), + commit_message: z.string(), +}); + +export const simplePullRequestSchema = z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + html_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + issue_url: z.string(), + number: z.number(), + state: z.union([z.literal("open"), z.literal("closed")]), + locked: z.boolean(), + title: z.string(), + user: userSchema, + body: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + merged_at: z.string().nullable(), + merge_commit_sha: z.string().nullable(), + assignee: userSchema.nullable(), + assignees: z.array(userSchema), + requested_reviewers: z.array(z.union([userSchema, teamSchema])), + requested_teams: z.array(teamSchema), + labels: z.array(labelSchema), + milestone: milestoneSchema.nullable(), + draft: z.boolean(), + commits_url: z.string(), + review_comments_url: z.string(), + review_comment_url: z.string(), + comments_url: z.string(), + statuses_url: z.string(), + head: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + base: z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: userSchema, + repo: repositorySchema, + }), + _links: z.object({ + self: linkSchema, + html: linkSchema, + issue: linkSchema, + comments: linkSchema, + review_comments: linkSchema, + review_comment: linkSchema, + commits: linkSchema, + statuses: linkSchema, + }), + author_association: authorAssociationSchema, + auto_merge: pullRequestAutoMergeSchema.nullable(), + active_lock_reason: z + .union([ + z.literal("resolved"), + z.literal("off-topic"), + z.literal("too heated"), + z.literal("spam"), + ]) + .nullable(), +}); + +export const committerSchema = z.object({ + name: z.string(), + email: z.string().nullable(), + date: z.string().optional(), + username: z.string().optional(), +}); + +export const commitSchema = z.object({ + id: z.string(), + tree_id: z.string(), + distinct: z.boolean(), + message: z.string(), + timestamp: z.string(), + url: z.string(), + author: committerSchema, + committer: committerSchema, + added: z.array(z.string()), + modified: z.array(z.string()), + removed: z.array(z.string()), +}); diff --git a/integrations/github/src/schemas/stars.ts b/integrations/github/src/schemas/stars.ts new file mode 100644 index 00000000000..490809391db --- /dev/null +++ b/integrations/github/src/schemas/stars.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; +import { organizationSchema, repositorySchema, userSchema } from "./shared"; + +export const starCreatedEventSchema = z.object({ + action: z.literal("created"), + starred_at: z.coerce.date(), + repository: repositorySchema, + sender: userSchema, + organization: organizationSchema.optional(), +}); + +export const starDeletedEventSchema = z.object({ + action: z.literal("deleted"), + repository: repositorySchema, + sender: userSchema, + organization: organizationSchema.optional(), +}); + +export const starEventSchema = z.discriminatedUnion("action", [ + starCreatedEventSchema, + starDeletedEventSchema, +]); diff --git a/integrations/github/src/types.ts b/integrations/github/src/types.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/integrations/github/src/webhooks.ts b/integrations/github/src/webhooks.ts new file mode 100644 index 00000000000..2277ce49c28 --- /dev/null +++ b/integrations/github/src/webhooks.ts @@ -0,0 +1,223 @@ +import { Webhooks } from "@octokit/webhooks"; +import crypto from "crypto"; +import { z } from "zod"; +import { getAccessToken } from "@trigger.dev/integration-sdk"; +import type { + DisplayProperty, + HandleWebhookOptions, + WebhookConfig, + WebhookIntegration, +} from "@trigger.dev/integration-sdk"; +import { + WebhookOrganizationSourceSchema, + WebhookRepoSourceSchema, + WebhookSourceSchema, +} from "./schemas"; + +export class GitHubWebhookIntegration implements WebhookIntegration { + keyForSource(source: unknown): string { + const githubSource = parseWebhookSource(source); + + if (githubSource.subresource === "repository") { + return `repository.${githubSource.repo}.${githubSource.events.join(".")}`; + } else if (githubSource.subresource === "organization") { + return `organization.${githubSource.org}`; + } else { + throw new Error(`Unknown subresource`); + } + } + + registerWebhook(config: WebhookConfig, source: unknown) { + const githubSource = parseWebhookSource(source); + + if (githubSource.subresource === "repository") { + return registerRepositoryWebhook(config, githubSource); + } else if (githubSource.subresource === "organization") { + return registerOrganizationWebhook(config, githubSource); + } else { + throw new Error(`Unknown subresource`); + } + } + + handleWebhookRequest(options: HandleWebhookOptions) { + const deliveryId = options.request.headers["x-github-delivery"]; + const hookId = options.request.headers["x-github-hook-id"]; + + const signature = options.request.headers["x-hub-signature-256"]; + + if (options.secret && signature) { + const githubWebhooks = new Webhooks({ + secret: options.secret, + }); + + console.log( + `[delivery=${deliveryId}][hook=${hookId}] GitHubWebhookIntegration: Verifying signature ${signature}` + ); + + if (!githubWebhooks.verify(options.request.body, signature)) { + return { + status: "error" as const, + error: `GitHubWebhookIntegration: Could not verify webhook payload, invalid signature or secret. [deliveryId = ${deliveryId}]]`, + }; + } + } + + const event = options.request.headers["x-github-event"]; + + const context = omit(options.request.headers, [ + "x-github-event", + "x-github-delivery", + "x-hub-signature-256", + "x-hub-signature", + "content-type", + "content-length", + "accept", + "accept-encoding", + "x-forwarded-proto", + ]); + + const id = md5Hash([hookId, deliveryId].join("-")); + + return { + status: "ok" as const, + data: { id, payload: options.request.body, event, context }, + }; + } + + displayProperties(source: unknown) { + const githubSource = parseWebhookSource(source); + + const title = `GitHub ${githubSource.events.join(", ")}`; + let properties: DisplayProperty[] = []; + + switch (githubSource.subresource) { + case "repository": { + properties = [ + { + key: "Repo", + value: githubSource.repo, + }, + ]; + break; + } + case "organization": { + properties = [ + { + key: "Organization", + value: githubSource.org, + }, + ]; + break; + } + default: { + throw new Error(`Unknown subresource`); + } + } + + return { title, properties }; + } +} + +export const webhooks = new GitHubWebhookIntegration(); + +async function registerRepositoryWebhook( + config: WebhookConfig, + source: z.infer +) { + // Create the webhook in github + const response = await fetch( + `https://api.github.com/repos/${source.repo}/hooks`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/vnd.github+json", + Authorization: `Bearer ${getAccessToken(config.accessInfo)}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + body: JSON.stringify({ + name: "web", + active: true, + events: source.events, + config: { + url: config.callbackUrl, + content_type: "json", + secret: config.secret, + insecure_ssl: "0", + }, + }), + } + ); + + if (!response.ok) { + throw new Error(`Failed to register webhook: ${response.statusText}`); + } + + const webhook = await response.json(); + + return webhook; +} + +async function registerOrganizationWebhook( + config: WebhookConfig, + source: z.infer +) { + // Create the webhook in github + const response = await fetch( + `https://api.github.com/orgs/${source.org}/hooks`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/vnd.github+json", + Authorization: `Bearer ${getAccessToken(config.accessInfo)}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + body: JSON.stringify({ + name: "web", + active: true, + events: source.events, + config: { + url: config.callbackUrl, + content_type: "json", + secret: config.secret, + insecure_ssl: "0", + }, + }), + } + ); + + if (!response.ok) { + throw new Error(`Failed to register webhook: ${response.statusText}`); + } + + const webhook = await response.json(); + + return webhook; +} + +function parseWebhookSource(source: unknown) { + return WebhookSourceSchema.parse(source); +} + +function omit, K extends keyof T>( + obj: T, + keys: K[] +): Omit { + const result: any = {}; + + for (const key of Object.keys(obj)) { + if (!keys.includes(key as K)) { + result[key] = obj[key]; + } + } + + return result; +} + +function md5Hash(str: string): string { + const hash = crypto.createHash("md5"); + hash.update(str); + + return hash.digest("hex"); +} diff --git a/integrations/github/tsconfig.json b/integrations/github/tsconfig.json new file mode 100644 index 00000000000..8437d79f48d --- /dev/null +++ b/integrations/github/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": ["./src/**/*.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ] + } + }, + "exclude": ["node_modules"] +} diff --git a/integrations/github/tsup.config.ts b/integrations/github/tsup.config.ts new file mode 100644 index 00000000000..a71244deb1a --- /dev/null +++ b/integrations/github/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + treeshake: { + preset: "smallest", + }, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + noExternal: ["@trigger.dev/common-schemas"], + }, +]); diff --git a/packages/integration-sdk/README.md b/packages/integration-sdk/README.md new file mode 100644 index 00000000000..22ddcc68b35 --- /dev/null +++ b/packages/integration-sdk/README.md @@ -0,0 +1 @@ +Docs coming soon. diff --git a/packages/integration-sdk/package.json b/packages/integration-sdk/package.json new file mode 100644 index 00000000000..e65f2ea08d4 --- /dev/null +++ b/packages/integration-sdk/package.json @@ -0,0 +1,30 @@ +{ + "name": "@trigger.dev/integration-sdk", + "version": "0.1.16", + "description": "trigger.dev SDK for integrations", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@trigger.dev/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/node": "16", + "@types/react": "^18.0.21", + "rimraf": "^3.0.2", + "tsup": "^6.5.0" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "dependencies": { + "debug": "^4.3.4", + "zod": "^3.20.2" + } +} \ No newline at end of file diff --git a/packages/integration-sdk/src/index.ts b/packages/integration-sdk/src/index.ts new file mode 100644 index 00000000000..aebc4b7a1fc --- /dev/null +++ b/packages/integration-sdk/src/index.ts @@ -0,0 +1,15 @@ +export * from "./types"; +export * from "./services"; + +import { AccessInfo } from "./types"; + +export function getAccessToken(accessInfo: AccessInfo): string { + switch (accessInfo.type) { + case "oauth2": { + return accessInfo.accessToken; + } + case "api_key": { + return accessInfo.api_key; + } + } +} diff --git a/packages/integration-sdk/src/services/index.ts b/packages/integration-sdk/src/services/index.ts new file mode 100644 index 00000000000..4985b159c42 --- /dev/null +++ b/packages/integration-sdk/src/services/index.ts @@ -0,0 +1,137 @@ +import { z } from "zod"; +import debug from "debug"; + +const log = debug("trigger:integrations:services"); + +export type HttpServiceOptions = { + accessToken: string; + baseUrl: string; +}; + +export type HttpResponse< + TResponseSchema extends z.ZodTypeAny, + TErrorResponseSchema extends z.ZodTypeAny = z.AnyZodObject +> = + | { + success: true; + statusCode: number; + headers: Record; + data: z.infer; + } + | { + success: false; + statusCode: number; + headers: Record; + error?: z.infer; + }; + +export class HttpService { + constructor(private readonly options: HttpServiceOptions) {} + + async performRequest< + TResponseSchema extends z.ZodTypeAny, + TBodySchema extends z.ZodTypeAny = z.ZodUndefined + >( + endpoint: HttpEndpoint, + body?: z.infer + ): Promise> { + const url = `${this.options.baseUrl}${endpoint.options.path}`; + const method = endpoint.options.method; + const headers = { + Authorization: `Bearer ${this.options.accessToken}`, + "Content-Type": "application/json; charset=utf-8", + }; + const actualBody = body ? JSON.stringify(body) : undefined; + + const response = await fetch(url, { + method, + headers, + body: actualBody, + }); + + log( + "%s%s response %d", + this.options.baseUrl, + endpoint.options.path, + response.status + ); + + const json = await this.#safeGetJson(response); + + if (json) { + log( + "%s%s response %O", + this.options.baseUrl, + endpoint.options.path, + json + ); + + const parsedJson = endpoint.options.response.safeParse(json); + + if (parsedJson.success) { + return { + success: true, + data: parsedJson.data, + statusCode: response.status, + headers: normalizeHeaders(response.headers), + }; + } + + log( + "response json failed to parse %O, errors: %O", + parsedJson, + parsedJson.error + ); + + return { + success: false, + statusCode: response.status, + headers: normalizeHeaders(response.headers), + error: { type: "zod-parse", issues: parsedJson.error.issues }, + }; + } + + return { + success: false, + statusCode: response.status, + headers: normalizeHeaders(response.headers), + }; + } + + #safeGetJson = async (response: Response) => { + try { + return await response.json(); + } catch (error) { + return undefined; + } + }; +} + +export type HttpEndpointOptions< + TResponseSchema extends z.ZodTypeAny, + TBodySchema extends z.ZodTypeAny = z.ZodUndefined +> = { + response: TResponseSchema; + body?: TBodySchema; + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + path: string; +}; + +export class HttpEndpoint< + TResponseSchema extends z.ZodTypeAny, + TBodySchema extends z.ZodTypeAny = z.ZodUndefined +> { + constructor( + public options: HttpEndpointOptions + ) {} +} + +function normalizeHeaders(headers: Headers): Record { + const normalizedHeaders: Record = {}; + + headers.forEach((value, key) => { + normalizedHeaders[key.toLowerCase()] = value; + }); + + return normalizedHeaders; +} diff --git a/packages/integration-sdk/src/types.ts b/packages/integration-sdk/src/types.ts new file mode 100644 index 00000000000..0bd63aa5bd7 --- /dev/null +++ b/packages/integration-sdk/src/types.ts @@ -0,0 +1,91 @@ +import type { ReactNode } from "react"; + +export type AccessInfo = + | { type: "oauth2"; accessToken: string } + | { + type: "api_key"; + api_key: string; + additionalFields?: Record; + }; + +export interface WebhookConfig { + accessInfo: AccessInfo; + callbackUrl: string; + secret: string; +} + +export interface NormalizedRequest { + rawBody: string; + body: any; + headers: Record; + searchParams: URLSearchParams; +} + +export interface NormalizedResponse { + output: NonNullable; + context: any; +} + +export interface HandleWebhookOptions { + request: NormalizedRequest; + secret?: string; +} + +export interface ReceivedWebhook { + id: string; + event: string; + payload: any; + timestamp?: string; + context?: any; +} + +export type PerformRequestOptions = { + accessInfo: AccessInfo; + endpoint: string; + params: any; + cache?: CacheService; + metadata?: Record; +}; + +export type DisplayProperties = { + title: string; + properties?: DisplayProperty[]; +}; + +export type DisplayProperty = { key: string; value: string | number | boolean }; + +export interface PerformedRequestResponse { + response: NormalizedResponse; + isRetryable: boolean; + ok: boolean; +} + +export interface RequestIntegration { + perform: ( + options: PerformRequestOptions + ) => Promise; + displayProperties: (endpoint: string, params: any) => DisplayProperties; + renderComponent(input: any, output: any): ReactNode; +} + +export interface WebhookIntegration { + keyForSource: (source: unknown) => string; + registerWebhook: (config: WebhookConfig, source: unknown) => Promise; + handleWebhookRequest: ( + options: HandleWebhookOptions + ) => + | { status: "ok"; data: ReceivedWebhook } + | { status: "ignored"; reason: string } + | { status: "error"; error: string }; + displayProperties: (source: unknown) => DisplayProperties; +} + +export interface CacheService { + get: (key: string) => Promise; + set: (key: string, value: string, ttl?: number) => Promise; +} + +export type InternalIntegration = { + requests?: RequestIntegration; + webhooks?: WebhookIntegration; +}; diff --git a/packages/integration-sdk/tsconfig.json b/packages/integration-sdk/tsconfig.json new file mode 100644 index 00000000000..4388aa73ad4 --- /dev/null +++ b/packages/integration-sdk/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2021"] + }, + "exclude": ["node_modules"] +} diff --git a/packages/integration-sdk/tsup.config.ts b/packages/integration-sdk/tsup.config.ts new file mode 100644 index 00000000000..74c4f4dc148 --- /dev/null +++ b/packages/integration-sdk/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + }, +]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e2455f4e70..f05255538ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,8 @@ importers: '@testing-library/react': ^13.4.0 '@testing-library/user-event': ^14.4.3 '@trigger.dev/common-schemas': workspace:* + '@trigger.dev/github': workspace:* + '@trigger.dev/integration-sdk': workspace:* '@trigger.dev/providers': workspace:* '@trigger.dev/tailwind-config': workspace:* '@typeform/embed-react': ^2.14.1 @@ -184,7 +186,7 @@ importers: '@aws-sdk/client-s3': 3.245.0 '@aws-sdk/s3-request-presigner': 3.245.0 '@cfworker/json-schema': 1.12.5 - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 @@ -208,9 +210,10 @@ importers: '@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8 '@tanstack/react-table': 8.7.6_biqbaboplfbrettd7655fr4n2y '@trigger.dev/common-schemas': link:../../packages/common-schemas + '@trigger.dev/github': link:../../integrations/github '@trigger.dev/providers': link:../../packages/trigger-providers '@typeform/embed-react': 2.14.1_react@18.2.0 - '@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne + '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle bcryptjs: 2.4.3 classnames: 2.3.2 clsx: 1.2.1 @@ -277,6 +280,7 @@ importers: '@testing-library/jest-dom': 5.16.5 '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y '@testing-library/user-event': 14.4.3_ua4wuun3fnehcjqtqj2e2ldgpe + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk '@trigger.dev/tailwind-config': link:../../config-packages/tailwind-config '@types/bcryptjs': 2.4.2 '@types/compression': 1.7.2 @@ -406,14 +410,14 @@ importers: examples/github-webhook: specifiers: - '@trigger.dev/integrations': workspace:* + '@trigger.dev/github': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' tsx: ^3.12.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations + '@trigger.dev/github': link:../../integrations/github '@trigger.dev/sdk': link:../../packages/trigger-sdk zod: 3.20.2 devDependencies: @@ -557,6 +561,27 @@ importers: '@types/node': 16.18.11 tsx: 3.12.2 + integrations/github: + specifiers: + '@octokit/webhooks': ^10.4.0 + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/node': '16' + rimraf: ^3.0.2 + tsup: ^6.5.0 + zod: ^3.20.2 + dependencies: + '@octokit/webhooks': 10.5.1 + zod: 3.20.2 + devDependencies: + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + rimraf: 3.0.2 + tsup: 6.5.0 + packages/common-schemas: specifiers: '@trigger.dev/tsconfig': workspace:* @@ -613,6 +638,27 @@ importers: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/react': 18.0.26 + packages/integration-sdk: + specifiers: + '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 + '@types/node': '16' + '@types/react': ^18.0.21 + debug: ^4.3.4 + rimraf: ^3.0.2 + tsup: ^6.5.0 + zod: ^3.20.2 + dependencies: + debug: 4.3.4 + zod: 3.20.2 + devDependencies: + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 + '@types/node': 16.18.11 + '@types/react': 18.0.26 + rimraf: 3.0.2 + tsup: 6.5.0 + packages/internal-bridge: specifiers: '@trigger.dev/common-schemas': workspace:* @@ -750,7 +796,7 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@react-email/render': 0.0.3_react@18.2.0 + '@react-email/render': 0.0.3 '@trigger.dev/providers': link:../trigger-providers '@trigger.dev/sdk': link:../trigger-sdk zod: 3.20.2 @@ -3457,13 +3503,12 @@ packages: prettier: 2.8.2 dev: false - /@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde: + /@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu: resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==} peerDependencies: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.3.2 '@codemirror/state': 6.2.0 @@ -3483,7 +3528,7 @@ packages: /@codemirror/lang-javascript/6.1.2: resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==} dependencies: - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/state': 6.2.0 @@ -4681,6 +4726,16 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false + /@react-email/render/0.0.3: + resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} + engines: {node: '>=18.0.0'} + dependencies: + pretty: 2.0.0 + react-dom: 18.2.0 + transitivePeerDependencies: + - react + dev: false + /@react-email/render/0.0.3_react@18.2.0: resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} engines: {node: '>=18.0.0'} @@ -4796,7 +4851,7 @@ packages: eslint: 8.31.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy - eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq + eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny eslint-plugin-jest: 26.9.0_ohsifnwenhmxgcp7mend4dnv74 eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0 @@ -5932,18 +5987,17 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e: + /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' - '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 @@ -5952,14 +6006,11 @@ packages: '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne: + /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: - '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' - '@codemirror/theme-one-dark': '>=6.0.0' '@codemirror/view': '>=6.0.0' - codemirror: '>=6.0.0' react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: @@ -5968,14 +6019,13 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e - codemirror: 6.0.1_@lezer+common@1.0.2 + '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom + codemirror: 6.0.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' - - '@codemirror/lint' - '@codemirror/search' dev: false @@ -7286,18 +7336,16 @@ packages: engines: {node: '>=0.10.0'} dev: false - /codemirror/6.0.1_@lezer+common@1.0.2: + /codemirror/6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: - '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde + '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 - transitivePeerDependencies: - - '@lezer/common' dev: false /collection-visit/1.0.0: @@ -8633,7 +8681,7 @@ packages: debug: 4.3.4 enhanced-resolve: 5.12.0 eslint: 8.31.0 - eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq + eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny get-tsconfig: 4.3.0 globby: 13.1.3 is-core-module: 2.11.0 @@ -8643,7 +8691,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama: + /eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -8668,6 +8716,7 @@ packages: debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 + eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy transitivePeerDependencies: - supports-color dev: true @@ -8692,7 +8741,7 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-import/2.27.4_qdjeohovcytra7xto5vgmxssaq: + /eslint-plugin-import/2.27.4_2ac3tknkazjoq5fxmuugu665ny: resolution: {integrity: sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==} engines: {node: '>=4'} peerDependencies: @@ -8710,7 +8759,7 @@ packages: doctrine: 2.1.0 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama + eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -13810,6 +13859,15 @@ packages: shallow-equal: 1.2.1 dev: false + /react-dom/18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + scheduler: 0.23.0 + dev: false + /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c54dbc83ec5..ad67a99193c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - "config-packages/*" - "packages/*" + - "integrations/*" - "apps/**" - "examples/*" From 2bb7bad9475a47605f5009957edd5d7903fb94fb Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 13:25:55 +0000 Subject: [PATCH 28/71] =?UTF-8?q?Breakout=20slack=20into=20it=E2=80=99s=20?= =?UTF-8?q?own=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../performIntegrationRequest.server.ts | 3 +- .../slack/handleInteractivity.server.ts | 4 +- apps/webapp/remix.config.js | 1 + apps/webapp/tsconfig.json | 4 +- config-packages/tsconfig/examples.json | 6 + examples/fetch-playground/package.json | 2 +- examples/fetch-playground/src/index.ts | 2 +- examples/schedule-to-slack/package.json | 2 +- examples/schedule-to-slack/src/index.ts | 10 +- examples/send-to-slack/package.json | 4 +- examples/send-to-slack/src/index.tsx | 2 +- examples/smoke-test/package.json | 3 +- integrations/github/src/internal.ts | 10 +- .../github/src/{ => internal}/webhooks.ts | 2 +- integrations/github/tsconfig.json | 2 +- integrations/slack/README.md | 3 + integrations/slack/manifest.json | 35 + integrations/slack/package.json | 41 + integrations/slack/src/blocks.ts | 861 ++++++++++++++++++ integrations/slack/src/events.ts | 37 + integrations/slack/src/index.ts | 97 ++ integrations/slack/src/interactivity.ts | 215 +++++ integrations/slack/src/internal.ts | 26 + integrations/slack/src/internal/requests.ts | 475 ++++++++++ integrations/slack/src/schemas.ts | 119 +++ integrations/slack/tsconfig.json | 20 + integrations/slack/tsup.config.ts | 23 + packages/integration-sdk/src/types.ts | 2 + pnpm-lock.yaml | 39 +- 29 files changed, 2018 insertions(+), 32 deletions(-) rename integrations/github/src/{ => internal}/webhooks.ts (99%) create mode 100644 integrations/slack/README.md create mode 100644 integrations/slack/manifest.json create mode 100644 integrations/slack/package.json create mode 100644 integrations/slack/src/blocks.ts create mode 100644 integrations/slack/src/events.ts create mode 100644 integrations/slack/src/index.ts create mode 100644 integrations/slack/src/interactivity.ts create mode 100644 integrations/slack/src/internal.ts create mode 100644 integrations/slack/src/internal/requests.ts create mode 100644 integrations/slack/src/schemas.ts create mode 100644 integrations/slack/tsconfig.json create mode 100644 integrations/slack/tsup.config.ts diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index b59b99f7501..5d3c1f5d027 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -4,7 +4,8 @@ import type { NormalizedResponse, PerformedRequestResponse, } from "@trigger.dev/integration-sdk"; -import { resend, shopify, slack } from "internal-integrations"; +import { resend, shopify } from "internal-integrations"; +import * as slack from "@trigger.dev/slack/internal"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; import type { IntegrationRequest } from "~/models/integrationRequest.server"; diff --git a/apps/webapp/app/services/slack/handleInteractivity.server.ts b/apps/webapp/app/services/slack/handleInteractivity.server.ts index 1d3b55c38a2..2d4a70cb921 100644 --- a/apps/webapp/app/services/slack/handleInteractivity.server.ts +++ b/apps/webapp/app/services/slack/handleInteractivity.server.ts @@ -1,4 +1,4 @@ -import { slack } from "@trigger.dev/providers"; +import { schemas } from "@trigger.dev/slack/internal"; import { ulid } from "ulid"; import { generateErrorMessage } from "zod-error"; import type { PrismaClient } from "~/db.server"; @@ -15,7 +15,7 @@ export class HandleSlackInteractivity { public async call(payload: unknown) { console.log("payload", JSON.stringify(payload, null, 2)); - const parsedPayload = slack.schemas.blockAction.safeParse(payload); + const parsedPayload = schemas.blockAction.safeParse(payload); if (!parsedPayload.success) { console.error( diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index 1bf20b954a3..f2026a32c2f 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -15,6 +15,7 @@ module.exports = { "internal-bridge", "@trigger.dev/providers", "@trigger.dev/github", + "@trigger.dev/slack", "@trigger.dev/common-schemas", "@trigger.dev/sdk", "@trigger.dev/integrations", diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index b9d37884b90..4333afa1487 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -51,7 +51,9 @@ "../../packages/integration-sdk/src/*" ], "@trigger.dev/github": ["../../integrations/github/src/index"], - "@trigger.dev/github/*": ["../../integrations/github/src/*"] + "@trigger.dev/github/*": ["../../integrations/github/src/*"], + "@trigger.dev/slack": ["../../integrations/slack/src/index"], + "@trigger.dev/slack/*": ["../../integrations/slack/src/*"] }, "noEmit": true } diff --git a/config-packages/tsconfig/examples.json b/config-packages/tsconfig/examples.json index 48479fab9f9..4481a47daa2 100644 --- a/config-packages/tsconfig/examples.json +++ b/config-packages/tsconfig/examples.json @@ -35,6 +35,12 @@ ], "@trigger.dev/github/*": [ "../../integrations/github/src/*" + ], + "@trigger.dev/slack": [ + "../../integrations/slack/src/index" + ], + "@trigger.dev/slack/*": [ + "../../integrations/slack/src/*" ] } } diff --git a/examples/fetch-playground/package.json b/examples/fetch-playground/package.json index 92c2b3eb230..2bffd84ef03 100644 --- a/examples/fetch-playground/package.json +++ b/examples/fetch-playground/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "description": "A fetch playground for testing Trigger.dev functionality", "dependencies": { - "@trigger.dev/integrations": "workspace:*", + "@trigger.dev/slack": "workspace:*", "@trigger.dev/sdk": "workspace:*", "zod": "^3.20.2" }, diff --git a/examples/fetch-playground/src/index.ts b/examples/fetch-playground/src/index.ts index e1874064ff8..7fe765244f6 100644 --- a/examples/fetch-playground/src/index.ts +++ b/examples/fetch-playground/src/index.ts @@ -1,4 +1,4 @@ -import { slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; import { Trigger, customEvent, webhookEvent } from "@trigger.dev/sdk"; import { z } from "zod"; diff --git a/examples/schedule-to-slack/package.json b/examples/schedule-to-slack/package.json index 16dfb7ad1e4..18fd2723af8 100644 --- a/examples/schedule-to-slack/package.json +++ b/examples/schedule-to-slack/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "description": "Send a message to slack on a schedule", "dependencies": { - "@trigger.dev/integrations": "workspace:*", + "@trigger.dev/slack": "workspace:*", "@trigger.dev/sdk": "workspace:*", "zod": "^3.20.2" }, diff --git a/examples/schedule-to-slack/src/index.ts b/examples/schedule-to-slack/src/index.ts index 6ca2262e233..a53e30df9ef 100644 --- a/examples/schedule-to-slack/src/index.ts +++ b/examples/schedule-to-slack/src/index.ts @@ -1,5 +1,5 @@ import { Trigger, scheduleEvent } from "@trigger.dev/sdk"; -import { slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; const trigger = new Trigger({ id: "schedule-to-slack-2", @@ -11,10 +11,10 @@ const trigger = new Trigger({ run: async (event, ctx) => { await ctx.logger.info("It's me, the annoying slack bot!"); - // const response = await slack.postMessage("slaaaaaack", { - // channelName: "test-integrations", - // text: `Hello, the time is ${event.scheduledTime}, and I was last run at ${event.lastRunAt}!`, - // }); + const response = await slack.postMessage("slaaaaaack", { + channelName: "test-integrations", + text: `Hello, the time is ${event.scheduledTime}, and I was last run at ${event.lastRunAt}!`, + }); return event; }, diff --git a/examples/send-to-slack/package.json b/examples/send-to-slack/package.json index 1f261d4ee88..daf53be7c47 100644 --- a/examples/send-to-slack/package.json +++ b/examples/send-to-slack/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "description": "Send a message to slack when a customer creates a new custom domain", "dependencies": { - "@trigger.dev/integrations": "workspace:*", + "@trigger.dev/slack": "workspace:*", "@trigger.dev/sdk": "workspace:*", "jsx-slack": "^5.3.0", "zod": "^3.20.2" @@ -17,4 +17,4 @@ "scripts": { "dev": "tsx src/index.tsx" } -} +} \ No newline at end of file diff --git a/examples/send-to-slack/src/index.tsx b/examples/send-to-slack/src/index.tsx index 7953c4031dc..ec738577bd1 100644 --- a/examples/send-to-slack/src/index.tsx +++ b/examples/send-to-slack/src/index.tsx @@ -1,5 +1,5 @@ import { Trigger, customEvent, scheduleEvent } from "@trigger.dev/sdk"; -import { slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; import JSXSlack, { Actions, Blocks, diff --git a/examples/smoke-test/package.json b/examples/smoke-test/package.json index 8f6881e9b30..cb792adfd0d 100644 --- a/examples/smoke-test/package.json +++ b/examples/smoke-test/package.json @@ -4,7 +4,6 @@ "version": "0.0.1", "description": "Very basic smoke test for @trigger.dev/sdk", "dependencies": { - "@trigger.dev/integrations": "workspace:*", "@trigger.dev/sdk": "workspace:*", "ulid": "^2.3.0", "zod": "^3.20.2" @@ -17,4 +16,4 @@ "scripts": { "dev": "tsx src/index.ts" } -} +} \ No newline at end of file diff --git a/integrations/github/src/internal.ts b/integrations/github/src/internal.ts index 02e61a23350..a4f3d91e9f0 100644 --- a/integrations/github/src/internal.ts +++ b/integrations/github/src/internal.ts @@ -1,7 +1,8 @@ -import { GitHubWebhookIntegration } from "./webhooks"; -import * as schemas from "./schemas"; +import { GitHubWebhookIntegration } from "./internal/webhooks"; -export const provider = { +export const webhooks = new GitHubWebhookIntegration(); + +export const metadata = { name: "GitHub", slug: "github", icon: "/integrations/github.png", @@ -10,7 +11,6 @@ export const provider = { type: "oauth", scopes: ["repo"], }, - schemas, }; -export const webhooks = new GitHubWebhookIntegration(); +export * as schemas from "./schemas"; diff --git a/integrations/github/src/webhooks.ts b/integrations/github/src/internal/webhooks.ts similarity index 99% rename from integrations/github/src/webhooks.ts rename to integrations/github/src/internal/webhooks.ts index 2277ce49c28..24dde60e1ba 100644 --- a/integrations/github/src/webhooks.ts +++ b/integrations/github/src/internal/webhooks.ts @@ -12,7 +12,7 @@ import { WebhookOrganizationSourceSchema, WebhookRepoSourceSchema, WebhookSourceSchema, -} from "./schemas"; +} from "../schemas"; export class GitHubWebhookIntegration implements WebhookIntegration { keyForSource(source: unknown): string { diff --git a/integrations/github/tsconfig.json b/integrations/github/tsconfig.json index 8437d79f48d..e159cec9c07 100644 --- a/integrations/github/tsconfig.json +++ b/integrations/github/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@trigger.dev/tsconfig/node16.json", - "include": ["./src/**/*.ts"], + "include": ["./src/**/*.ts", "tsup.config.ts"], "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/integrations/slack/README.md b/integrations/slack/README.md new file mode 100644 index 00000000000..1ad0e149738 --- /dev/null +++ b/integrations/slack/README.md @@ -0,0 +1,3 @@ +# Trigger.dev GitHub integration + +View more documentation [here](https://docs.trigger.dev) diff --git a/integrations/slack/manifest.json b/integrations/slack/manifest.json new file mode 100644 index 00000000000..2f63a247e93 --- /dev/null +++ b/integrations/slack/manifest.json @@ -0,0 +1,35 @@ +{ + "display_information": { + "name": "Trigger.dev" + }, + "features": { + "bot_user": { + "display_name": "Trigger.dev", + "always_online": false + } + }, + "oauth_config": { + "redirect_urls": [ + "https://auth.trigger.dev/oauth/callback" + ], + "scopes": { + "user": [ + "channels:read", + "groups:read", + "im:read", + "mpim:read", + "chat:write", + "reactions:write" + ], + "bot": [ + "chat:write", + "reactions:write" + ] + } + }, + "settings": { + "org_deploy_enabled": false, + "socket_mode_enabled": false, + "token_rotation_enabled": false + } +} \ No newline at end of file diff --git a/integrations/slack/package.json b/integrations/slack/package.json new file mode 100644 index 00000000000..d07c09b9d48 --- /dev/null +++ b/integrations/slack/package.json @@ -0,0 +1,41 @@ +{ + "name": "@trigger.dev/slack", + "version": "0.1.16", + "description": "The official Slack integration for Trigger.dev", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@trigger.dev/integration-sdk": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/node": "16", + "rimraf": "^3.0.2", + "tsup": "^6.5.0" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:*" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "dependencies": { + "@octokit/webhooks": "^10.4.0", + "debug": "^4.3.4", + "zod": "^3.20.2" + } +} \ No newline at end of file diff --git a/integrations/slack/src/blocks.ts b/integrations/slack/src/blocks.ts new file mode 100644 index 00000000000..514b9725df9 --- /dev/null +++ b/integrations/slack/src/blocks.ts @@ -0,0 +1,861 @@ +import { z } from "zod"; + +export const imageElementSchema = z.object({ + type: z.literal("image"), + image_url: z.string(), + alt_text: z.string(), +}); + +export const plainTextElementSchema = z.object({ + type: z.literal("plain_text"), + text: z.string(), + emoji: z.boolean().optional(), +}); + +export const mrkdwnElementSchema = z.object({ + type: z.literal("mrkdwn"), + text: z.string(), + verbatim: z.boolean().optional(), +}); + +export const mrkdwnOptionSchema = z.object({ + text: mrkdwnElementSchema, + value: z.string().optional(), + url: z.string().optional(), + description: plainTextElementSchema.optional(), +}); + +export const plainTextOptionSchema = z.object({ + text: plainTextElementSchema, + value: z.string().optional(), + url: z.string().optional(), + description: plainTextElementSchema.optional(), +}); + +export const optionSchema = z.union([ + mrkdwnOptionSchema, + plainTextOptionSchema, +]); + +export const confirmSchema = z.object({ + title: plainTextElementSchema.optional(), + text: z.discriminatedUnion("type", [ + plainTextElementSchema, + mrkdwnElementSchema, + ]), + confirm: plainTextElementSchema.optional(), + deny: plainTextElementSchema.optional(), + style: z.union([z.literal("primary"), z.literal("danger")]).optional(), +}); + +/** + * @description Determines when an input element will return a + * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` interaction payload}. + */ +export const dispatchActionConfigSchema = z.object({ + /** + * @description An array of interaction types that you would like to receive a + * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload} for. Should be + * one or both of: + * `on_enter_pressed` — payload is dispatched when user presses the enter key while the input is in focus. Hint + * text will appear underneath the input explaining to the user to press enter to submit. + * `on_character_entered` — payload is dispatched when a character is entered (or removed) in the input. + */ + trigger_actions_on: z + .array( + z.union([ + z.literal("on_enter_pressed"), + z.literal("on_character_entered"), + ]) + ) + .optional(), +}); + +export const actionSchema = z.object({ + type: z.string(), + /** + * @description: An identifier for this action. You can use this when you receive an interaction payload to + * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique + * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. + */ + action_id: z.string().optional(), +}); + +export const actionIdSchema = z.object({ + /** + * @description: An identifier for this action. You can use this when you receive an interaction payload to + * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique + * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. + */ + action_id: z.string().optional(), +}); + +export const confirmableSchema = z.object({ + /** + * @description A {@see Confirm} object that defines an optional confirmation dialog after the element is interacted + * with. + */ + confirm: confirmSchema.optional(), +}); + +export const focusableSchema = z.object({ + /** + * @description Indicates whether the element will be set to auto focus within the + * {@link https://api.slack.com/reference/surfaces/views `view` object}. Only one element can be set to `true`. + * Defaults to `false`. + */ + focus_on_load: z.boolean().optional(), +}); + +export const placeholdableSchema = z.object({ + /** + * @description A {@see PlainTextElement} object that defines the placeholder text shown on the element. Maximum + * length for the `text` field in this object is 150 characters. + */ + placeholder: plainTextElementSchema.optional(), +}); + +export const dispatchableSchema = z.object({ + /** + * @description A {@see DispatchActionConfig} object that determines when during text input the element returns a + * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload}. + */ + dispatch_action_config: dispatchActionConfigSchema.optional(), +}); + +export const usersSelectSchema = z + .object({ + type: z.literal("users_select"), + initial_user: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiUsersSelectSchema = z + .object({ + type: z.literal("multi_users_select"), + initial_users: z.array(z.string()).optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const staticSelectSchema = z + .object({ + type: z.literal("static_select"), + initial_option: plainTextOptionSchema.optional(), + options: z.array(plainTextOptionSchema).optional(), + option_groups: z + .array( + z.object({ + label: plainTextElementSchema, + options: z.array(plainTextOptionSchema), + }) + ) + .optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiStaticSelectSchema = z + .object({ + type: z.literal("multi_static_select"), + initial_options: z.array(plainTextOptionSchema).optional(), + options: z.array(plainTextOptionSchema).optional(), + option_groups: z + .array( + z.object({ + label: plainTextElementSchema, + options: z.array(plainTextOptionSchema), + }) + ) + .optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const conversationsSelectSchema = z + .object({ + type: z.literal("conversations_select"), + initial_conversation: z.string().optional(), + response_url_enabled: z.boolean().optional(), + default_to_current_conversation: z.boolean().optional(), + filter: z + .object({ + include: z + .array( + z.union([ + z.literal("im"), + z.literal("mpim"), + z.literal("private"), + z.literal("public"), + ]) + ) + .optional(), + exclude_external_shared_channels: z.boolean().optional(), + exclude_bot_users: z.boolean().optional(), + }) + .optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiConversationsSelectSchema = z + .object({ + type: z.literal("multi_conversations_select"), + initial_conversations: z.array(z.string()).optional(), + max_selected_items: z.number().optional(), + default_to_current_conversation: z.boolean().optional(), + filter: z + .object({ + include: z + .array( + z.union([ + z.literal("im"), + z.literal("mpim"), + z.literal("private"), + z.literal("public"), + ]) + ) + .optional(), + exclude_external_shared_channels: z.boolean().optional(), + exclude_bot_users: z.boolean().optional(), + }) + .optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const channelsSelectSchema = z + .object({ + type: z.literal("channels_select"), + initial_channel: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiChannelsSelectSchema = z + .object({ + type: z.literal("multi_channels_select"), + initial_channels: z.array(z.string()).optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const externalSelectSchema = z + .object({ + type: z.literal("external_select"), + initial_option: plainTextOptionSchema.optional(), + min_query_length: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const multiExternalSelectSchema = z + .object({ + type: z.literal("multi_external_select"), + initial_options: z.array(plainTextOptionSchema).optional(), + min_query_length: z.number().optional(), + max_selected_items: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const buttonSchema = z + .object({ + type: z.literal("button"), + text: plainTextElementSchema, + value: z.string().optional(), + url: z.string().optional(), + style: z.union([z.literal("danger"), z.literal("primary")]).optional(), + accessibility_label: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema); + +export const overflowSchema = z + .object({ + type: z.literal("overflow"), + options: z.array(plainTextOptionSchema), + }) + .merge(actionIdSchema) + .merge(confirmableSchema); + +export const datepickerSchema = z + .object({ + type: z.literal("datepicker"), + initial_date: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const timepickerSchema = z + .object({ + type: z.literal("timepicker"), + initial_time: z.string().optional(), + timezone: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const radioButtonsSchema = z + .object({ + type: z.literal("radio_buttons"), + initial_option: optionSchema.optional(), + options: z.array(optionSchema), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema); + +/** + * @description An element that allows the selection of a time of day formatted as a UNIX timestamp. On desktop + * clients, this time picker will take the form of a dropdown list merge the date picker will take the form of a dropdown + * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker merge date + * picker will use native UIs. + * {@link https://api.slack.com/reference/block-kit/block-elements#datetimepicker} + */ +export const dateTimepickerSchema = z + .object({ + type: z.literal("datetimepicker"), + /** + * @description The initial date merge time that is selected when the element is loaded, represented as a UNIX + * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date merge + * time August 10th, 2021 at 03:17pm PST. + */ + initial_date_time: z.number().optional(), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema); + +export const checkboxesSchema = z + .object({ + type: z.literal("checkboxes"), + initial_options: z.array(optionSchema).optional(), + options: z.array(optionSchema), + }) + .merge(actionIdSchema) + .merge(confirmableSchema) + .merge(focusableSchema); + +export const plainTextInputSchema = z + .object({ + type: z.literal("plain_text_input"), + initial_value: z.string().optional(), + multiline: z.boolean().optional(), + min_length: z.number().optional(), + max_length: z.number().optional(), + dispatch_action_config: dispatchActionConfigSchema.optional(), + focus_on_load: z.boolean().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +/** + * @description A URL input element, similar to the {@see PlainTextInput} element, creates a single line field where + * a user can enter URL-encoded data. + * {@link https://api.slack.com/reference/block-kit/block-elements#url} + */ +export const uRLInputSchema = z + .object({ + type: z.literal("url_text_input"), + /** + * @description The initial value in the URL input when it is loaded. + */ + initial_value: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +/** + * @description An email input element, similar to the {@see PlainTextInput} element, creates a single line field where + * a user can enter an email address. + * {@link https://api.slack.com/reference/block-kit/block-elements#email} + */ +export const emailInputSchema = z + .object({ + type: z.literal("email_text_input"), + /** + * @description The initial value in the email input when it is loaded. + */ + initial_value: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +/** + * @description A number input element, similar to the {@see PlainTextInput} element, creates a single line field where + * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, merge -10 are all + * valid input values. Decimal numbers are only allowed when `is_decimal_allowed` is equal to `true`. + * {@link https://api.slack.com/reference/block-kit/block-elements#number} + */ +export const numberInputSchema = z + .object({ + type: z.literal("number_input"), + /** + * @description Decimal numbers are allowed if this property is `true`, set the value to `false` otherwise. + */ + is_decimal_allowed: z.boolean(), + /** + * @description The initial value in the input when it is loaded. + */ + initial_value: z.string().optional(), + /** + * @description The minimum value, cannot be greater than `max_value`. + */ + min_value: z.string().optional(), + /** + * @description The maximum value, cannot be less than `min_value`. + */ + max_value: z.string().optional(), + }) + .merge(actionIdSchema) + .merge(dispatchableSchema) + .merge(focusableSchema) + .merge(placeholdableSchema); + +export const blockSchema = z.object({ + type: z.string(), + block_id: z.string().optional(), +}); + +export const imageBlockSchema = blockSchema.extend({ + type: z.literal("image"), + image_url: z.string(), + alt_text: z.string(), + title: plainTextElementSchema.optional(), +}); + +export const contextBlockSchema = blockSchema.extend({ + type: z.literal("context"), + elements: z.array( + z.discriminatedUnion("type", [ + imageElementSchema, + plainTextElementSchema, + mrkdwnElementSchema, + ]) + ), +}); + +export const dividerBlockSchema = blockSchema.extend({ + type: z.literal("divider"), +}); + +export const fileBlockSchema = blockSchema.extend({ + type: z.literal("file"), + source: z.string(), + external_id: z.string(), +}); + +export const headerBlockSchema = blockSchema.extend({ + type: z.literal("header"), + text: plainTextElementSchema, +}); + +export const messageMetadataEventPayloadObjectSchema = z.record( + z.union([z.string(), z.number(), z.boolean()]) +); + +export const messageAttachmentPreviewSchema = z.object({ + type: z.string().optional(), + can_remove: z.boolean().optional(), + title: plainTextElementSchema.optional(), + subtitle: plainTextElementSchema.optional(), + iconUrl: z.string().optional(), +}); + +export const optionFieldSchema = z.object({ + description: z.string().optional(), + text: z.string(), + value: z.string(), +}); + +export const confirmationSchema = z.object({ + dismiss_text: z.string().optional(), + ok_text: z.string().optional(), + text: z.string(), + title: z.string().optional(), +}); + +export const selectOptionSchema = z.object({ + label: z.string(), + value: z.string(), +}); + +export const callUserSlackSchema = z.object({ + slack_id: z.string(), +}); + +export const callUserExternalSchema = z.object({ + external_id: z.string(), + display_name: z.string(), + avatar_url: z.string(), +}); + +export const videoBlockSchema = blockSchema.extend({ + type: z.literal("video"), + video_url: z.string(), + thumbnail_url: z.string(), + alt_text: z.string(), + title: plainTextElementSchema, + title_url: z.string().optional(), + author_name: z.string().optional(), + provider_name: z.string().optional(), + provider_icon_url: z.string().optional(), + description: plainTextElementSchema.optional(), +}); + +export const dialogSchema = z.object({ + title: z.string(), + callback_id: z.string(), + elements: z.array( + z.object({ + type: z.union([ + z.literal("text"), + z.literal("textarea"), + z.literal("select"), + ]), + name: z.string(), + label: z.string(), + optional: z.boolean().optional(), + placeholder: z.string().optional(), + value: z.string().optional(), + max_length: z.number().optional(), + min_length: z.number().optional(), + hint: z.string().optional(), + subtype: z + .union([ + z.literal("email"), + z.literal("number"), + z.literal("tel"), + z.literal("url"), + ]) + .optional(), + data_source: z + .union([ + z.literal("users"), + z.literal("channels"), + z.literal("conversations"), + z.literal("external"), + ]) + .optional(), + selected_options: z.array(selectOptionSchema).optional(), + options: z.array(selectOptionSchema).optional(), + option_groups: z + .array( + z.object({ + label: z.string(), + options: z.array(selectOptionSchema), + }) + ) + .optional(), + min_query_length: z.number().optional(), + }) + ), + submit_label: z.string().optional(), + notify_on_cancel: z.boolean().optional(), + state: z.string().optional(), +}); + +const selectSchemas = [ + usersSelectSchema, + staticSelectSchema, + conversationsSelectSchema, + channelsSelectSchema, + externalSelectSchema, +]; + +export const selectSchema = z.discriminatedUnion("type", [ + usersSelectSchema, + staticSelectSchema, + conversationsSelectSchema, + channelsSelectSchema, + externalSelectSchema, +]); + +const multiSelectSchemas = [ + multiUsersSelectSchema, + multiStaticSelectSchema, + multiConversationsSelectSchema, + multiChannelsSelectSchema, + multiExternalSelectSchema, +]; +export const multiSelectSchema = z.discriminatedUnion("type", [ + multiUsersSelectSchema, + multiStaticSelectSchema, + multiConversationsSelectSchema, + multiChannelsSelectSchema, + multiExternalSelectSchema, +]); + +export const actionsBlockSchema = blockSchema.extend({ + type: z.literal("actions"), + elements: z.array( + z.discriminatedUnion("type", [ + buttonSchema, + overflowSchema, + datepickerSchema, + timepickerSchema, + dateTimepickerSchema, + ...selectSchemas, + radioButtonsSchema, + checkboxesSchema, + ]) + ), +}); + +export const sectionBlockSchema = blockSchema.extend({ + type: z.literal("section"), + text: z + .discriminatedUnion("type", [plainTextElementSchema, mrkdwnElementSchema]) + .optional(), + fields: z + .array( + z.discriminatedUnion("type", [ + plainTextElementSchema, + mrkdwnElementSchema, + ]) + ) + .optional(), + accessory: z + .discriminatedUnion("type", [ + buttonSchema, + overflowSchema, + datepickerSchema, + timepickerSchema, + ...selectSchemas, + ...multiSelectSchemas, + imageElementSchema, + radioButtonsSchema, + checkboxesSchema, + ]) + .optional(), +}); + +export const inputBlockSchema = blockSchema.extend({ + type: z.literal("input"), + label: plainTextElementSchema, + hint: plainTextElementSchema.optional(), + optional: z.boolean().optional(), + element: z.discriminatedUnion("type", [ + datepickerSchema, + ...selectSchemas, + ...multiSelectSchemas, + timepickerSchema, + dateTimepickerSchema, + plainTextInputSchema, + uRLInputSchema, + emailInputSchema, + numberInputSchema, + radioButtonsSchema, + checkboxesSchema, + ]), + dispatch_action: z.boolean().optional(), +}); + +export const messageMetadataSchema = z.object({ + event_type: z.string(), + event_payload: z.record( + z.union([ + z.string(), + z.number(), + z.boolean(), + messageMetadataEventPayloadObjectSchema, + z.array(messageMetadataEventPayloadObjectSchema), + ]) + ), +}); + +export const attachmentActionSchema = z.object({ + id: z.string().optional(), + confirm: confirmationSchema.optional(), + data_source: z + .union([ + z.literal("static"), + z.literal("channels"), + z.literal("conversations"), + z.literal("users"), + z.literal("external"), + ]) + .optional(), + min_query_length: z.number().optional(), + name: z.string().optional(), + options: z.array(optionFieldSchema).optional(), + option_groups: z + .array( + z.object({ + text: z.string(), + options: z.array(optionFieldSchema), + }) + ) + .optional(), + selected_options: z.array(optionFieldSchema).optional(), + style: z + .union([z.literal("default"), z.literal("primary"), z.literal("danger")]) + .optional(), + text: z.string(), + type: z.union([z.literal("button"), z.literal("select")]), + value: z.string().optional(), + url: z.string().optional(), +}); + +export const callUserSchema = z.union([ + callUserSlackSchema, + callUserExternalSchema, +]); + +const knownBlocks = [ + imageBlockSchema, + contextBlockSchema, + actionsBlockSchema, + dividerBlockSchema, + sectionBlockSchema, + inputBlockSchema, + fileBlockSchema, + headerBlockSchema, + videoBlockSchema, +]; + +export const knownBlockSchema = z.discriminatedUnion("type", [ + imageBlockSchema, + contextBlockSchema, + actionsBlockSchema, + dividerBlockSchema, + sectionBlockSchema, + inputBlockSchema, + fileBlockSchema, + headerBlockSchema, + videoBlockSchema, +]); + +const anyBlockSchema = z.discriminatedUnion("type", [ + imageBlockSchema, + contextBlockSchema, + actionsBlockSchema, + dividerBlockSchema, + sectionBlockSchema, + inputBlockSchema, + fileBlockSchema, + headerBlockSchema, + videoBlockSchema, +]); + +export const messageAttachmentSchema = z.object({ + blocks: z.array(anyBlockSchema).optional(), + fallback: z.string().optional(), + color: z + .union([ + z.literal("good"), + z.literal("warning"), + z.literal("danger"), + z.string(), + ]) + .optional(), + pretext: z.string().optional(), + author_name: z.string().optional(), + author_link: z.string().optional(), + author_icon: z.string().optional(), + title: z.string().optional(), + title_link: z.string().optional(), + text: z.string().optional(), + fields: z + .array( + z.object({ + title: z.string(), + value: z.string(), + short: z.boolean().optional(), + }) + ) + .optional(), + image_url: z.string().optional(), + thumb_url: z.string().optional(), + footer: z.string().optional(), + footer_icon: z.string().optional(), + ts: z.string().optional(), + actions: z.array(attachmentActionSchema).optional(), + callback_id: z.string().optional(), + mrkdwn_in: z + .array( + z.union([z.literal("pretext"), z.literal("text"), z.literal("fields")]) + ) + .optional(), + app_unfurl_url: z.string().optional(), + is_app_unfurl: z.boolean().optional(), + app_id: z.string().optional(), + bot_id: z.string().optional(), + preview: messageAttachmentPreviewSchema.optional(), +}); + +export const linkUnfurlsSchema = z.record(messageAttachmentSchema); + +export const homeViewSchema = z.object({ + type: z.literal("home"), + blocks: z.array(anyBlockSchema), + private_metadata: z.string().optional(), + callback_id: z.string().optional(), + external_id: z.string().optional(), +}); + +export const modalViewSchema = z.object({ + type: z.literal("modal"), + title: plainTextElementSchema, + blocks: z.array(anyBlockSchema), + close: plainTextElementSchema.optional(), + submit: plainTextElementSchema.optional(), + private_metadata: z.string().optional(), + callback_id: z.string().optional(), + clear_on_close: z.boolean().optional(), + notify_on_close: z.boolean().optional(), + external_id: z.string().optional(), +}); + +export const workflowStepViewSchema = z.object({ + type: z.literal("workflow_step"), + blocks: z.array(anyBlockSchema), + private_metadata: z.string().optional(), + callback_id: z.string().optional(), + submit_disabled: z.boolean().optional(), + external_id: z.string().optional(), +}); + +export const viewSchema: z.ZodDiscriminatedUnion< + "type", + [typeof homeViewSchema, typeof modalViewSchema, typeof workflowStepViewSchema] +> = z.discriminatedUnion("type", [ + homeViewSchema, + modalViewSchema, + workflowStepViewSchema, +]); diff --git a/integrations/slack/src/events.ts b/integrations/slack/src/events.ts new file mode 100644 index 00000000000..50c847c38f3 --- /dev/null +++ b/integrations/slack/src/events.ts @@ -0,0 +1,37 @@ +import type { TriggerEvent } from "@trigger.dev/sdk"; +import { blockAction } from "./interactivity"; + +export function blockActionInteraction(params: { + blockId: string; + actionId?: string | string[]; +}): TriggerEvent { + const actionIds = + typeof params.actionId === "undefined" + ? [] + : Array.isArray(params.actionId) + ? params.actionId + : [params.actionId]; + + return { + metadata: { + type: "SLACK_INTERACTION", + service: "slack", + name: "block.action", + filter: { + service: ["slack"], + payload: { + actions: { + block_id: [params.blockId], + action_id: actionIds, + }, + }, + event: ["block.action"], + }, + source: { + blockId: params.blockId, + actionIds, + }, + }, + schema: blockAction, + }; +} diff --git a/integrations/slack/src/index.ts b/integrations/slack/src/index.ts new file mode 100644 index 00000000000..15aa1c0ea9e --- /dev/null +++ b/integrations/slack/src/index.ts @@ -0,0 +1,97 @@ +import { getTriggerRun } from "@trigger.dev/sdk"; +import { z } from "zod"; +import * as events from "./events"; +import * as schemas from "./schemas"; + +export { events }; + +export type PostMessageOptions = z.infer< + typeof schemas.PostMessageOptionsSchema +>; + +export type PostMessageResponse = z.infer< + typeof schemas.PostMessageSuccessResponseSchema +>; + +export async function postMessage( + key: string, + message: PostMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call postMessage outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "slack", + endpoint: "chat.postMessage", + params: message, + response: { + schema: schemas.PostMessageSuccessResponseSchema, + }, + }); + + return output; +} + +export type PostMessageResponseOptions = z.infer< + typeof schemas.PostMessageResponseOptionsSchema +>; + +export type PostMessageResponseResponse = z.infer< + typeof schemas.PostMessageResponseSuccessResponseSchema +>; + +export async function postMessageResponse( + key: string, + responseUrl: string, + message: PostMessageResponseOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call postMessageResponse outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "slack", + endpoint: "chat.postMessageResponse", + params: { message, responseUrl }, + response: { + schema: schemas.PostMessageResponseSuccessResponseSchema, + }, + }); + + return output; +} + +export type AddReactionOptions = z.infer< + typeof schemas.AddReactionOptionsSchema +>; + +export type AddReactionResponse = z.infer< + typeof schemas.AddReactionSuccessResponseSchema +>; + +export async function addReaction( + key: string, + options: AddReactionOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call addReaction outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "slack", + endpoint: "reactions.add", + params: options, + response: { + schema: schemas.AddReactionSuccessResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/slack/src/interactivity.ts b/integrations/slack/src/interactivity.ts new file mode 100644 index 00000000000..2e341d605e3 --- /dev/null +++ b/integrations/slack/src/interactivity.ts @@ -0,0 +1,215 @@ +import { z } from "zod"; +import { + knownBlockSchema, + mrkdwnElementSchema, + optionFieldSchema, + plainTextElementSchema, + viewSchema, +} from "./blocks"; + +const textSchema = z.discriminatedUnion("type", [ + plainTextElementSchema, + mrkdwnElementSchema, +]); +const blockActionType = z.union([ + z.literal("block_actions"), + z.literal("interactive_message"), +]); + +const sourceType = z.literal("message"); + +const commonActionSchema = z.object({ + action_id: z.string(), + block_id: z.string(), + action_ts: z.string(), +}); + +const buttonAction = z.object({ + type: z.literal("button"), + text: textSchema.optional(), + value: z.string(), +}); + +const selectedOptionSchema = z.object({ + text: z.object({ + type: z.string(), + text: z.string(), + emoji: z.boolean().optional(), + }), + value: z.string(), +}); +const placeholderSchema = z.object({ + type: z.string(), + text: z.string(), + emoji: z.boolean(), +}); +const staticSelectAction = z.object({ + type: z.literal("static_select"), + selected_option: selectedOptionSchema.nullable(), + placeholder: placeholderSchema.optional(), +}); + +const userSelectAction = z.object({ + type: z.literal("users_select"), + selected_user: z.string().nullable(), + initial_user: z.string().optional(), +}); + +const conversationsSelectAction = z.object({ + type: z.literal("conversations_select"), + selected_conversation: z.string().nullable(), + initial_conversation: z.string().optional(), +}); +const channelSelectAction = z.object({ + type: z.literal("channels_select"), + selected_channel: z.string().nullable(), + initial_channel: z.string().optional(), +}); + +const datePickerAction = z.object({ + type: z.literal("datepicker"), + selected_date: z.string().nullable(), + initial_date: z.string().optional(), +}); + +const checkboxesAction = z.object({ + type: z.literal("checkboxes"), + selected_options: z.array(optionFieldSchema), +}); + +const radioButtonsSchema = z.object({ + type: z.literal("radio_buttons"), + selectedOption: optionFieldSchema, +}); + +const timePickerSchema = z.object({ + type: z.literal("timepicker"), + selected_time: z.string().nullable(), + initial_time: z.string().optional(), +}); + +const plainTextInputSchema = z.object({ + type: z.literal("plain_text_input"), + value: z.string().nullable(), + initial_value: z.string().optional(), +}); + +const multiUsersSelectSchema = z.object({ + type: z.literal("multi_users_select"), + selected_users: z.array(z.string()), + initial_users: z.array(z.string()).optional(), +}); + +const multiStaticSelectSchema = z.object({ + type: z.literal("multi_static_select"), + selected_options: z.array(selectedOptionSchema), + placeholder: placeholderSchema.optional(), +}); + +const possibleActionsSchema = z.discriminatedUnion("type", [ + buttonAction, + staticSelectAction, + userSelectAction, + conversationsSelectAction, + channelSelectAction, + datePickerAction, + checkboxesAction, + radioButtonsSchema, + timePickerSchema, + plainTextInputSchema, + multiUsersSelectSchema, + multiStaticSelectSchema, +]); + +const actionSchema = possibleActionsSchema.and(commonActionSchema); + +//state.values.issue.action.block.rating.selected_option +const stateSchema = z.object({ + values: z.record(z.record(possibleActionsSchema)), +}); + +const userSchema = z.object({ + id: z.string(), + username: z.string(), + name: z.string(), + team_id: z.string(), +}); + +const containerSchema = z.object({ + type: sourceType, + message_ts: z.string(), + channel_id: z.string(), + is_ephemeral: z.boolean(), +}); + +const teamSchema = z.object({ id: z.string(), domain: z.string() }); +const channelSchema = z.object({ id: z.string(), name: z.string() }); + +const viewActionDataSchema = z.object({ + id: z.string(), + team_id: z.string(), + state: z + .object({ + values: z.record(z.record(z.any())), + }) + .optional(), + hash: z.string(), + previous_view_id: z.string().optional(), + root_view_id: z.string().optional(), + app_id: z.string().optional(), + app_installed_team_id: z.string().optional(), + bot_id: z.string().optional(), +}); +const viewActionSchema: Zod.ZodIntersection< + typeof viewSchema, + typeof viewActionDataSchema +> = viewSchema.and(viewActionDataSchema); + +const messageActionSchema = z.object({ + bot_id: z.string(), + type: sourceType, + text: z.string().optional(), + user: z.string().optional(), + ts: z.string(), + app_id: z.string().optional(), + blocks: z.array(knownBlockSchema).optional(), + team: z.string().optional(), + metadata: z + .object({ + event_type: z.string(), + event_payload: z.object({ requestId: z.string() }), + }) + .optional(), +}); + +export const blockAction: Zod.ZodObject<{ + type: typeof blockActionType; + user: typeof userSchema; + api_app_id: Zod.ZodString; + container: typeof containerSchema; + trigger_id: z.ZodOptional; + team: typeof teamSchema; + enterprise: Zod.ZodAny; + is_enterprise_install: Zod.ZodBoolean; + channel: typeof channelSchema; + view: z.ZodOptional; + message: z.ZodOptional; + state: z.ZodOptional; + response_url: Zod.ZodString; + actions: Zod.ZodArray; +}> = z.object({ + type: blockActionType, + user: userSchema, + api_app_id: z.string(), + container: containerSchema, + trigger_id: z.string().optional(), + team: teamSchema, + enterprise: z.any(), + is_enterprise_install: z.boolean(), + channel: channelSchema, + view: viewActionSchema.optional(), + message: messageActionSchema.optional(), + state: stateSchema.optional(), + response_url: z.string(), + actions: z.array(actionSchema), +}); diff --git a/integrations/slack/src/internal.ts b/integrations/slack/src/internal.ts new file mode 100644 index 00000000000..ee226571167 --- /dev/null +++ b/integrations/slack/src/internal.ts @@ -0,0 +1,26 @@ +import { SlackRequestIntegration } from "./internal/requests"; + +export const requests = new SlackRequestIntegration(); + +export const metadata = { + name: "Slack", + slug: "slack", + icon: "/integrations/slack.png", + enabledFor: "all", + authentication: { + type: "oauth", + scopes: [ + "channels:read", + "channels:join", + "channels:manage", + "chat:write", + "groups:write", + "im:write", + "mpim:write", + "chat:write.customize", + "reactions:write", + ], + }, +}; + +export * as schemas from "./schemas"; diff --git a/integrations/slack/src/internal/requests.ts b/integrations/slack/src/internal/requests.ts new file mode 100644 index 00000000000..457e8c7f226 --- /dev/null +++ b/integrations/slack/src/internal/requests.ts @@ -0,0 +1,475 @@ +import { HttpEndpoint, HttpService } from "@trigger.dev/integration-sdk"; +import type { + DisplayProperties, + CacheService, + PerformedRequestResponse, + PerformRequestOptions, + RequestIntegration, + AccessInfo, + ReactNode, +} from "@trigger.dev/integration-sdk"; +import debug from "debug"; +import { getAccessToken } from "@trigger.dev/integration-sdk"; +import { z } from "zod"; +import { + AddReactionOptionsSchema, + AddReactionResponseSchema, + ChannelNameOrIdSchema, + JoinConversationBodySchema, + JoinConversationResponseSchema, + ListConversationsResponseSchema, + PostMessageBodySchema, + PostMessageOptionsSchema, + PostMessageResponseOptionsSchema, + PostMessageResponseSchema, +} from "../schemas"; + +const log = debug("trigger:integrations:slack"); + +const SendSlackMessageRequestBodySchema = PostMessageBodySchema.extend({ + link_names: z.literal(1), + metadata: z + .object({ event_type: z.string(), event_payload: z.any() }) + .optional(), +}); + +export class SlackRequestIntegration implements RequestIntegration { + #joinChannelEndpoint = new HttpEndpoint< + typeof JoinConversationResponseSchema, + typeof JoinConversationBodySchema + >({ + response: JoinConversationResponseSchema, + method: "POST", + path: "/conversations.join", + }); + + #listConversationsEndpoint = new HttpEndpoint({ + response: ListConversationsResponseSchema, + method: "GET", + path: "/conversations.list", + }); + + #postMessageEndpoint = new HttpEndpoint< + typeof PostMessageResponseSchema, + typeof SendSlackMessageRequestBodySchema + >({ + response: PostMessageResponseSchema, + method: "POST", + path: "/chat.postMessage", + }); + + #addReactionEndpoint = new HttpEndpoint< + typeof AddReactionResponseSchema, + typeof AddReactionOptionsSchema + >({ + response: AddReactionResponseSchema, + method: "POST", + path: "/reactions.add", + }); + + constructor(private readonly baseUrl: string = "https://slack.com/api") {} + + perform(options: PerformRequestOptions): Promise { + switch (options.endpoint) { + case "chat.postMessage": { + return this.#postMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "chat.postMessageResponse": { + return this.#postMessageResponse( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "reactions.add": { + return this.#addReaction( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + default: { + throw new Error(`Unknown endpoint: ${options.endpoint}`); + } + } + } + + displayProperties(endpoint: string, params: any): DisplayProperties { + switch (endpoint) { + case "chat.postMessage": { + return { + title: `Post message to ${ + "channelName" in params ? params.channelName : params.channelId + }`, + properties: [ + { + key: "Text", + value: params.text, + }, + ], + }; + } + case "chat.postMessageResponse": { + return { + title: `Post response`, + properties: [], + }; + } + case "reactions.add": { + return { + title: `Add reaction to message ${params.timestamp}`, + properties: [ + { + key: "Reaction", + value: params.name, + }, + ], + }; + } + + default: { + throw new Error(`Unknown endpoint: ${endpoint}`); + } + } + } + + renderComponent(input: any, output: any): ReactNode { + return null; + } + + async #postMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = PostMessageOptionsSchema.parse(params); + + log("chat.postMessage %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: this.baseUrl, + }); + + const channelId = await this.#findChannelId(service, params, cache); + + if (!channelId) { + return { + ok: false, + isRetryable: false, + response: { + output: { + message: `channelId not found`, + }, + context: { + statusCode: 404, + headers: {}, + }, + }, + }; + } + + log("found channelId %s", channelId); + + const response = await service.performRequest(this.#postMessageEndpoint, { + ...parsedParams, + link_names: 1, + channel: channelId, + metadata: metadata + ? { event_type: "post_message", event_payload: metadata } + : undefined, + }); + + if (!response.success) { + log("chat.postMessage failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + if (!response.data.ok && response.data.error === "not_in_channel") { + log( + "chat.postMessage failed with not_in_channel, attempting to join channel %s", + channelId + ); + + // Attempt to join the channel, and then retry the request + const joinResponse = await service.performRequest( + this.#joinChannelEndpoint, + { + channel: channelId, + } + ); + + if (joinResponse.success && joinResponse.data.ok) { + log("joined channel %s, retrying postMessage", channelId); + + return this.#postMessage(accessInfo, params); + } + } + + const ok = response.data.ok; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + async #addReaction( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = AddReactionOptionsSchema.parse(params); + + log("reactions.add %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: this.baseUrl, + }); + + const channelId = await this.#findChannelId(service, parsedParams, cache); + + if (!channelId) { + return { + ok: false, + isRetryable: false, + response: { + output: { + message: `channelId not found`, + }, + context: { + statusCode: 404, + headers: {}, + }, + }, + }; + } + + log("found channelId %s", channelId); + + const response = await service.performRequest(this.#addReactionEndpoint, { + ...parsedParams, + // @ts-ignore + channel: channelId, + }); + + if (!response.success) { + log("reactions.add failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + if (!response.data.ok && response.data.error === "not_in_channel") { + log( + "reactions.add failed with not_in_channel, attempting to join channel %s", + channelId + ); + + // Attempt to join the channel, and then retry the request + const joinResponse = await service.performRequest( + this.#joinChannelEndpoint, + { + channel: channelId, + } + ); + + if (joinResponse.success && joinResponse.data.ok) { + log("joined channel %s, retrying reactions.add", channelId); + + return this.#addReaction(accessInfo, params); + } + } + + const ok = response.data.ok; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + async #postMessageResponse( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = z + .object({ + message: PostMessageResponseOptionsSchema, + responseUrl: z.string(), + }) + .parse(params); + + log("chat.postMessageResponse %O", parsedParams); + + const response = await fetch(parsedParams.responseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...parsedParams.message, + metadata: metadata + ? { event_type: "post_message_response", event_payload: metadata } + : undefined, + }), + }); + + if (!response.ok) { + log("chat.postMessageResponse failed %O", response); + + const error = await safeGetJson(response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.status), + response: { + output: error + ? error + : { name: `${response.status}`, message: response.statusText }, + context: { + statusCode: response.status, + headers: response.headers, + }, + }, + }; + } + + const output = await safeGetJson(response); + + const performedRequest = { + ok: response.ok, + isRetryable: this.#isRetryable(response.status), + response: { + output, + context: { + statusCode: response.status, + headers: response.headers, + }, + }, + }; + + log("chat.postMessage performedRequest %O", performedRequest); + + return performedRequest; + } + + #isRetryable(statusCode: number): boolean { + return ( + statusCode === 408 || + statusCode === 429 || + statusCode === 500 || + statusCode === 502 || + statusCode === 503 || + statusCode === 504 + ); + } + + // Will use the conversations.list API (using fetch) to find the channel ID + // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ") + async #findChannelId( + service: HttpService, + params: z.infer, + cache?: CacheService + ): Promise { + if ("channelId" in params) { + return params.channelId; + } + + if (!("channelName" in params)) { + throw new Error("Invalid params, mising channelId and channelName"); + } + + //if the channelName starts with a #, remove it + if (params.channelName.startsWith("#")) { + params.channelName = params.channelName.substring(1); + } + + const cachedChannelId = await cache?.get(params.channelName); + + if (cachedChannelId) { + return cachedChannelId; + } + + const response = await service.performRequest( + this.#listConversationsEndpoint + ); + + if (response.success && response.data.ok) { + const { channels } = response.data; + + const channelInfo = channels.find( + (c: any) => c.name === params.channelName + ); + + if (channelInfo) { + await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24); + return channelInfo.id; + } + } + + return undefined; + } +} + +function safeGetJson(response: Response): Promise { + return response.json().catch(() => null); +} diff --git a/integrations/slack/src/schemas.ts b/integrations/slack/src/schemas.ts new file mode 100644 index 00000000000..c1fe80518cd --- /dev/null +++ b/integrations/slack/src/schemas.ts @@ -0,0 +1,119 @@ +import { z } from "zod"; +import { knownBlockSchema } from "./blocks"; +import { blockAction } from "./interactivity"; + +export { blockAction }; + +export const PostMessageSuccessResponseSchema = z.object({ + ok: z.literal(true), + channel: z.string(), + ts: z.string(), + message: z.object({ + text: z.string(), + user: z.string().optional(), + bot_id: z.string(), + attachments: z.array(z.unknown()).optional(), + type: z.string(), + subtype: z.string().optional(), + ts: z.string(), + }), +}); + +export const ErrorResponseSchema = z.object({ + ok: z.literal(false), + error: z.string(), +}); + +export const PostMessageResponseSchema = z.discriminatedUnion("ok", [ + PostMessageSuccessResponseSchema, + ErrorResponseSchema, +]); + +export const PostMessageBodySchema = z.object({ + channel: z.string(), + text: z.string(), + blocks: z.array(knownBlockSchema).optional(), + username: z.string().optional(), + icon_emoji: z.string().optional(), + icon_url: z.string().optional(), +}); + +export const ChannelNameOrIdSchema = z.union([ + z.object({ channelId: z.string() }), + z.object({ channelName: z.string() }), +]); + +export const PostMessageOptionsSchema = z + .object({ + text: z.string(), + blocks: z.array(knownBlockSchema).optional(), + username: z.string().optional(), + icon_emoji: z.string().optional(), + icon_url: z.string().optional(), + }) + .and(ChannelNameOrIdSchema); + +export const AddReactionOptionsSchema = z + .object({ + name: z.string(), + timestamp: z.string(), + }) + .and(ChannelNameOrIdSchema); + +export const JoinConversationSuccessResponseSchema = z.object({ + ok: z.literal(true), + channel: z.object({ + id: z.string(), + }), +}); + +export const JoinConversationResponseSchema = z.discriminatedUnion("ok", [ + JoinConversationSuccessResponseSchema, + ErrorResponseSchema, +]); + +export const JoinConversationBodySchema = z.object({ + channel: z.string(), +}); + +export const ListConversationsSuccessResponseSchema = z.object({ + ok: z.literal(true), + channels: z.array( + z.object({ + id: z.string(), + name: z.string(), + }) + ), +}); + +export const ListConversationsResponseSchema = z.discriminatedUnion("ok", [ + ListConversationsSuccessResponseSchema, + ErrorResponseSchema, +]); + +export const PostMessageResponseOptionsSchema = z.object({ + text: z.string().optional(), + blocks: z.array(knownBlockSchema).optional(), + response_type: z.enum(["in_channel"]).optional(), + replace_original: z.boolean().optional(), + delete_original: z.boolean().optional(), + thread_ts: z.string().optional(), +}); + +export const PostMessageResponseSuccessResponseSchema = z.object({ + ok: z.literal(true), +}); + +export const PostMessageResponseResponseSchema = z.discriminatedUnion("ok", [ + PostMessageResponseSuccessResponseSchema, + ErrorResponseSchema, +]); + +export const AddReactionSuccessResponseSchema = z.object({ + ok: z.literal(true), +}); + +export const AddReactionResponseSchema = z.discriminatedUnion("ok", [ + AddReactionSuccessResponseSchema, + ErrorResponseSchema, +]); diff --git a/integrations/slack/tsconfig.json b/integrations/slack/tsconfig.json new file mode 100644 index 00000000000..e159cec9c07 --- /dev/null +++ b/integrations/slack/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ] + } + }, + "exclude": ["node_modules"] +} diff --git a/integrations/slack/tsup.config.ts b/integrations/slack/tsup.config.ts new file mode 100644 index 00000000000..a71244deb1a --- /dev/null +++ b/integrations/slack/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + treeshake: { + preset: "smallest", + }, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + noExternal: ["@trigger.dev/common-schemas"], + }, +]); diff --git a/packages/integration-sdk/src/types.ts b/packages/integration-sdk/src/types.ts index 0bd63aa5bd7..7ae10ae7394 100644 --- a/packages/integration-sdk/src/types.ts +++ b/packages/integration-sdk/src/types.ts @@ -1,5 +1,7 @@ import type { ReactNode } from "react"; +export type { ReactNode }; + export type AccessInfo = | { type: "oauth2"; accessToken: string } | { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f05255538ef..608b74c3d19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -393,15 +393,15 @@ importers: examples/fetch-playground: specifiers: - '@trigger.dev/integrations': workspace:* '@trigger.dev/sdk': workspace:* + '@trigger.dev/slack': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' tsx: ^3.12.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/slack': link:../../integrations/slack zod: 3.20.2 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -491,15 +491,15 @@ importers: examples/schedule-to-slack: specifiers: - '@trigger.dev/integrations': workspace:* '@trigger.dev/sdk': workspace:* + '@trigger.dev/slack': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' tsx: ^3.12.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/slack': link:../../integrations/slack zod: 3.20.2 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -508,16 +508,16 @@ importers: examples/send-to-slack: specifiers: - '@trigger.dev/integrations': workspace:* '@trigger.dev/sdk': workspace:* + '@trigger.dev/slack': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' jsx-slack: ^5.3.0 tsx: ^3.12.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/slack': link:../../integrations/slack jsx-slack: 5.3.0 zod: 3.20.2 devDependencies: @@ -544,7 +544,6 @@ importers: examples/smoke-test: specifiers: - '@trigger.dev/integrations': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' @@ -552,7 +551,6 @@ importers: ulid: ^2.3.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk ulid: 2.3.0 zod: 3.20.2 @@ -582,6 +580,31 @@ importers: rimraf: 3.0.2 tsup: 6.5.0 + integrations/slack: + specifiers: + '@octokit/webhooks': ^10.4.0 + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 + '@types/node': '16' + debug: ^4.3.4 + rimraf: ^3.0.2 + tsup: ^6.5.0 + zod: ^3.20.2 + dependencies: + '@octokit/webhooks': 10.5.1 + debug: 4.3.4 + zod: 3.20.2 + devDependencies: + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 + '@types/node': 16.18.11 + rimraf: 3.0.2 + tsup: 6.5.0 + packages/common-schemas: specifiers: '@trigger.dev/tsconfig': workspace:* From 09142cedd1d02c30709d209acf4923ef649e29f4 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 13:29:52 +0000 Subject: [PATCH 29/71] Moved internal only dependencies to devDependencies --- integrations/github/package.json | 2 +- integrations/slack/package.json | 1 - pnpm-lock.yaml | 7 +------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/integrations/github/package.json b/integrations/github/package.json index d2c15196ec9..fd7f78b9e4a 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -20,6 +20,7 @@ "@trigger.dev/sdk": "workspace:*", "@trigger.dev/tsconfig": "workspace:*", "@trigger.dev/integration-sdk": "workspace:*", + "@octokit/webhooks": "^10.4.0", "@types/node": "16", "rimraf": "^3.0.2", "tsup": "^6.5.0" @@ -33,7 +34,6 @@ "build:tsup": "tsup" }, "dependencies": { - "@octokit/webhooks": "^10.4.0", "zod": "^3.20.2" } } \ No newline at end of file diff --git a/integrations/slack/package.json b/integrations/slack/package.json index d07c09b9d48..eb6d39ffc68 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -34,7 +34,6 @@ "build:tsup": "tsup" }, "dependencies": { - "@octokit/webhooks": "^10.4.0", "debug": "^4.3.4", "zod": "^3.20.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 608b74c3d19..169413979c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -570,9 +570,9 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@octokit/webhooks': 10.5.1 zod: 3.20.2 devDependencies: + '@octokit/webhooks': 10.5.1 '@trigger.dev/integration-sdk': link:../../packages/integration-sdk '@trigger.dev/sdk': link:../../packages/trigger-sdk '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -582,7 +582,6 @@ importers: integrations/slack: specifiers: - '@octokit/webhooks': ^10.4.0 '@trigger.dev/integration-sdk': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* @@ -593,7 +592,6 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@octokit/webhooks': 10.5.1 debug: 4.3.4 zod: 3.20.2 devDependencies: @@ -4615,11 +4613,9 @@ packages: /@octokit/webhooks-methods/3.0.2: resolution: {integrity: sha512-Vlnv5WBscf07tyAvfDbp7pTkMZUwk7z7VwEF32x6HqI+55QRwBTcT+D7DDjZXtad/1dU9E32x0HmtDlF9VIRaQ==} engines: {node: '>= 14'} - dev: false /@octokit/webhooks-types/6.7.0: resolution: {integrity: sha512-bykm7UkSnxmb2uhSfcLM1Pity/LQ6ZBSdzy9HU0vXjR+2g+tzlmRhXb7Go8oj0TlgO+vDrTivGXju6zkzOGKjA==} - dev: false /@octokit/webhooks/10.5.1: resolution: {integrity: sha512-6yGcfVbQGIkRm/vlU4Ld4r0YNOT6ly5Kp+t6R3UAT0amxGASFtZLzOJry0UQfvvidCxK5qT9jHXU0QUp6alatA==} @@ -4629,7 +4625,6 @@ packages: '@octokit/webhooks-methods': 3.0.2 '@octokit/webhooks-types': 6.7.0 aggregate-error: 3.1.0 - dev: false /@open-draft/until/1.0.3: resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} From 5f517a5c7090c0410108890a172291a75bb492d3 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 13:52:56 +0000 Subject: [PATCH 30/71] =?UTF-8?q?Breakout=20resend=20into=20it=E2=80=99s?= =?UTF-8?q?=20own=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../performIntegrationRequest.server.ts | 3 +- apps/webapp/remix.config.js | 3 + apps/webapp/tsconfig.json | 4 +- config-packages/tsconfig/examples.json | 6 + examples/resend/package.json | 4 +- examples/resend/src/index.tsx | 6 +- integrations/resend/README.md | 3 + integrations/resend/package.json | 42 ++++ integrations/resend/src/index.ts | 60 ++++++ integrations/resend/src/internal.ts | 21 ++ integrations/resend/src/internal/requests.tsx | 203 ++++++++++++++++++ integrations/resend/src/schemas.ts | 45 ++++ integrations/resend/tsconfig.json | 26 +++ integrations/resend/tsup.config.ts | 23 ++ integrations/slack/README.md | 2 +- pnpm-lock.yaml | 31 ++- 16 files changed, 472 insertions(+), 10 deletions(-) create mode 100644 integrations/resend/README.md create mode 100644 integrations/resend/package.json create mode 100644 integrations/resend/src/index.ts create mode 100644 integrations/resend/src/internal.ts create mode 100644 integrations/resend/src/internal/requests.tsx create mode 100644 integrations/resend/src/schemas.ts create mode 100644 integrations/resend/tsconfig.json create mode 100644 integrations/resend/tsup.config.ts diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index 5d3c1f5d027..cf91ff0ed44 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -4,8 +4,9 @@ import type { NormalizedResponse, PerformedRequestResponse, } from "@trigger.dev/integration-sdk"; -import { resend, shopify } from "internal-integrations"; +import { shopify } from "internal-integrations"; import * as slack from "@trigger.dev/slack/internal"; +import * as resend from "@trigger.dev/resend/internal"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; import type { IntegrationRequest } from "~/models/integrationRequest.server"; diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index f2026a32c2f..32376cb37aa 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -16,6 +16,7 @@ module.exports = { "@trigger.dev/providers", "@trigger.dev/github", "@trigger.dev/slack", + "@trigger.dev/resend", "@trigger.dev/common-schemas", "@trigger.dev/sdk", "@trigger.dev/integrations", @@ -36,6 +37,8 @@ module.exports = { "../../packages/trigger-sdk/src/**/*", "../../packages/trigger-integrations/src/**/*", "../../integrations/github/src/**/*", + "../../integrations/slack/src/**/*", + "../../integrations/resend/src/**/*", ]; }, }; diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index 4333afa1487..c48723502ae 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -53,7 +53,9 @@ "@trigger.dev/github": ["../../integrations/github/src/index"], "@trigger.dev/github/*": ["../../integrations/github/src/*"], "@trigger.dev/slack": ["../../integrations/slack/src/index"], - "@trigger.dev/slack/*": ["../../integrations/slack/src/*"] + "@trigger.dev/slack/*": ["../../integrations/slack/src/*"], + "@trigger.dev/resend": ["../../integrations/resend/src/index"], + "@trigger.dev/resend/*": ["../../integrations/resend/src/*"] }, "noEmit": true } diff --git a/config-packages/tsconfig/examples.json b/config-packages/tsconfig/examples.json index 4481a47daa2..9b86ed09280 100644 --- a/config-packages/tsconfig/examples.json +++ b/config-packages/tsconfig/examples.json @@ -41,6 +41,12 @@ ], "@trigger.dev/slack/*": [ "../../integrations/slack/src/*" + ], + "@trigger.dev/resend": [ + "../../integrations/resend/src/index" + ], + "@trigger.dev/resend/*": [ + "../../integrations/resend/src/*" ] } } diff --git a/examples/resend/package.json b/examples/resend/package.json index 7fcd115597f..27f74dfba25 100644 --- a/examples/resend/package.json +++ b/examples/resend/package.json @@ -16,7 +16,7 @@ "@react-email/render": "^0.0.3", "@react-email/section": "^0.0.1", "@react-email/text": "^0.0.2", - "@trigger.dev/integrations": "workspace:*", + "@trigger.dev/resend": "workspace:*", "@trigger.dev/sdk": "workspace:*", "react": "^18.2.0", "zod": "^3.20.2" @@ -31,4 +31,4 @@ "scripts": { "dev": "tsx src/index.tsx" } -} +} \ No newline at end of file diff --git a/examples/resend/src/index.tsx b/examples/resend/src/index.tsx index c783d8b66ce..f0dbcebadfe 100644 --- a/examples/resend/src/index.tsx +++ b/examples/resend/src/index.tsx @@ -1,5 +1,5 @@ import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { resend } from "@trigger.dev/integrations"; +import * as resend from "@trigger.dev/resend"; import { Head } from "@react-email/head"; import { Html } from "@react-email/html"; import { Link } from "@react-email/link"; @@ -13,7 +13,7 @@ import { z } from "zod"; new Trigger({ id: "resend", name: "Resend text/html", - apiKey: "trigger_development_YpG7UQygbuT3", + apiKey: "trigger_development_Du0Tt9gYwsVg", endpoint: "ws://localhost:8889/ws", logLevel: "debug", on: customEvent({ @@ -48,7 +48,7 @@ new Trigger({ new Trigger({ id: "resend-react", name: "Resend React", - apiKey: "trigger_development_YpG7UQygbuT3", + apiKey: "trigger_development_Du0Tt9gYwsVg", endpoint: "ws://localhost:8889/ws", logLevel: "debug", on: customEvent({ diff --git a/integrations/resend/README.md b/integrations/resend/README.md new file mode 100644 index 00000000000..495467d9aaf --- /dev/null +++ b/integrations/resend/README.md @@ -0,0 +1,3 @@ +# Trigger.dev resend.com integration + +View more documentation [here](https://docs.trigger.dev) diff --git a/integrations/resend/package.json b/integrations/resend/package.json new file mode 100644 index 00000000000..306f5c8e1a7 --- /dev/null +++ b/integrations/resend/package.json @@ -0,0 +1,42 @@ +{ + "name": "@trigger.dev/resend", + "version": "0.1.16", + "description": "The official resend.com integration for Trigger.dev", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@trigger.dev/integration-sdk": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/node": "16", + "@types/react": "^18.0.26", + "rimraf": "^3.0.2", + "tsup": "^6.5.0" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:*" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "dependencies": { + "debug": "^4.3.4", + "zod": "^3.20.2", + "@react-email/render": "^0.0.3" + } +} \ No newline at end of file diff --git a/integrations/resend/src/index.ts b/integrations/resend/src/index.ts new file mode 100644 index 00000000000..150210629e5 --- /dev/null +++ b/integrations/resend/src/index.ts @@ -0,0 +1,60 @@ +import { getTriggerRun } from "@trigger.dev/sdk"; +import { z } from "zod"; +import { render } from "@react-email/render"; +import { + SendEmailOptionsSchema, + SendEmailSuccessResponseSchema, +} from "./schemas"; + +export type SendEmailOptions = z.infer; + +export type SendEmailResponse = z.infer; + +export async function sendEmail( + key: string, + message: SendEmailOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendEmail outside of a trigger run"); + } + + //if the supplied email body is react, we need to turn it into html + let html: string | undefined = undefined; + if (message.react) { + html = render(message.react, { + pretty: true, + }); + } else if (message.html) { + html = message.html; + } + + let params: any; + if (html) { + params = { + ...message, + reply_to: message.replyTo, + html, + }; + } else { + params = { + ...message, + reply_to: message.replyTo, + }; + delete params.html; + } + delete params.replyTo; + delete params.react; + + const output = await run.performRequest(key, { + service: "resend", + endpoint: "email.send", + params, + response: { + schema: SendEmailSuccessResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/resend/src/internal.ts b/integrations/resend/src/internal.ts new file mode 100644 index 00000000000..ee361ce6369 --- /dev/null +++ b/integrations/resend/src/internal.ts @@ -0,0 +1,21 @@ +import { ResendRequestIntegration } from "./internal/requests"; + +export const requests = new ResendRequestIntegration(); + +export * as schemas from "./schemas"; + +export const metadata = { + name: "Resend", + slug: "resend", + icon: "/integrations/resend.png", + enabledFor: "all", + authentication: { + type: "api_key", + header_name: "Authorization", + header_type: "access_token", + documentation: `1. Login to [Resend](https://resend.com) +2. Go to the API Keys page +3. Generate a new API key +4. Paste it into the field below`, + }, +}; diff --git a/integrations/resend/src/internal/requests.tsx b/integrations/resend/src/internal/requests.tsx new file mode 100644 index 00000000000..125f43607e0 --- /dev/null +++ b/integrations/resend/src/internal/requests.tsx @@ -0,0 +1,203 @@ +import { + HttpEndpoint, + HttpService, + getAccessToken, +} from "@trigger.dev/integration-sdk"; +import type { + DisplayProperties, + CacheService, + PerformedRequestResponse, + PerformRequestOptions, + RequestIntegration, + AccessInfo, +} from "@trigger.dev/integration-sdk"; +import debug from "debug"; +import React from "react"; +import { SendEmailResponseSchema, SendEmailBodySchema } from "../schemas"; + +const log = debug("trigger:integrations:resend"); + +export class ResendRequestIntegration implements RequestIntegration { + #sendEmailEndpoint = new HttpEndpoint< + typeof SendEmailResponseSchema, + typeof SendEmailBodySchema + >({ + response: SendEmailResponseSchema, + method: "POST", + path: "/email", + }); + + constructor(private readonly baseUrl: string = "https://api.resend.com") {} + + perform(options: PerformRequestOptions): Promise { + switch (options.endpoint) { + case "email.send": { + return this.#sendEmail( + options.accessInfo, + options.params, + options.cache + ); + } + default: { + throw new Error(`Unknown endpoint: ${options.endpoint}`); + } + } + } + + displayProperties(endpoint: string, params: any): DisplayProperties { + switch (endpoint) { + case "email.send": { + const parsed = SendEmailBodySchema.parse(params); + return { + title: `Send email to ${ + typeof parsed.to === "string" ? parsed.to : parsed.to.join(", ") + }`, + properties: [], + }; + } + default: { + throw new Error(`Unknown endpoint: ${endpoint}`); + } + } + } + + renderComponent(input: any, output: any): React.ReactNode { + const parsedInput = SendEmailBodySchema.parse(input); + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + {parsedInput.from} +

    +

    {parsedInput.subject}

    +
    + + + +
    +
    + +
    +
    +
    + {parsedInput.html && ( +
    + )} + {parsedInput.text && ( +
    {parsedInput.text}
    + )} +
    +
    + ); + } + + async #sendEmail( + accessInfo: AccessInfo, + params: any, + cache?: CacheService + ): Promise { + const parsedParams = SendEmailBodySchema.parse(params); + + log("email.send %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: this.baseUrl, + }); + + const response = await service.performRequest(this.#sendEmailEndpoint, { + ...parsedParams, + }); + + if (!response.success) { + log("email.send failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: {}, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + if (response.statusCode !== 200) { + log("email.send failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const performedRequest = { + ok: response.success, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("email.send performedRequest %O", performedRequest); + + return performedRequest; + } + + #isRetryable(statusCode: number): boolean { + return ( + statusCode === 408 || + statusCode === 429 || + statusCode === 500 || + statusCode === 502 || + statusCode === 503 || + statusCode === 504 + ); + } +} + +function EmailInfo({ + label, + value, +}: { + label: string; + value?: string | string[]; +}) { + if (!value) { + return null; + } + + return ( +
    +

    {label}:

    + {typeof value === "string" ? value : value.join(", ")} +
    + ); +} diff --git a/integrations/resend/src/schemas.ts b/integrations/resend/src/schemas.ts new file mode 100644 index 00000000000..3eb6dd9a5d2 --- /dev/null +++ b/integrations/resend/src/schemas.ts @@ -0,0 +1,45 @@ +import type { ReactElement } from "react"; +import { z } from "zod"; + +const ReactElementSchema = z.custom(); + +export const BaseSendFieldsSchema = z.object({ + from: z.string(), + to: z.union([z.string(), z.array(z.string())]), + bcc: z.union([z.string(), z.array(z.string())]).optional(), + cc: z.union([z.string(), z.array(z.string())]).optional(), + replyTo: z.union([z.string(), z.array(z.string())]).optional(), + subject: z.string().optional(), +}); + +export const SendEmailOptionsSchema = z + .object({ + text: z.string().optional(), + html: z.string().optional(), + react: ReactElementSchema.optional(), + }) + .and(BaseSendFieldsSchema); + +export const SendEmailBodySchema = BaseSendFieldsSchema.omit({ + replyTo: true, +}).and( + z.object({ + reply_to: z.string().optional(), + text: z.string().optional(), + html: z.string().optional(), + }) +); + +export const SendEmailSuccessResponseSchema = z.object({ + id: z.string(), + from: z.string(), + to: z.string(), + created_at: z.string().optional(), +}); +export const SendEmailErrorResponseSchema = z.object({ + error: z.object({ code: z.number(), type: z.string(), message: z.string() }), +}); +export const SendEmailResponseSchema = z.union([ + SendEmailSuccessResponseSchema, + SendEmailErrorResponseSchema, +]); diff --git a/integrations/resend/tsconfig.json b/integrations/resend/tsconfig.json new file mode 100644 index 00000000000..3c7114830c1 --- /dev/null +++ b/integrations/resend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": [ + "./src/**/*.ts", + "tsup.config.ts", + "src/internal/internal.tsxernal.tsx", + "src/internal/internal.tsxernal.tsx" + ], + "compilerOptions": { + "jsx": "react", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ] + } + }, + "exclude": ["node_modules"] +} diff --git a/integrations/resend/tsup.config.ts b/integrations/resend/tsup.config.ts new file mode 100644 index 00000000000..a71244deb1a --- /dev/null +++ b/integrations/resend/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + treeshake: { + preset: "smallest", + }, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + noExternal: ["@trigger.dev/common-schemas"], + }, +]); diff --git a/integrations/slack/README.md b/integrations/slack/README.md index 1ad0e149738..c7a7382c892 100644 --- a/integrations/slack/README.md +++ b/integrations/slack/README.md @@ -1,3 +1,3 @@ -# Trigger.dev GitHub integration +# Trigger.dev Slack integration View more documentation [here](https://docs.trigger.dev) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 169413979c7..13d29da2280 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -456,7 +456,7 @@ importers: '@react-email/render': ^0.0.3 '@react-email/section': ^0.0.1 '@react-email/text': ^0.0.2 - '@trigger.dev/integrations': workspace:* + '@trigger.dev/resend': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' @@ -478,7 +478,7 @@ importers: '@react-email/render': 0.0.3_react@18.2.0 '@react-email/section': 0.0.1 '@react-email/text': 0.0.2 - '@trigger.dev/integrations': link:../../packages/trigger-integrations + '@trigger.dev/resend': link:../../integrations/resend '@trigger.dev/sdk': link:../../packages/trigger-sdk react: 18.2.0 zod: 3.20.2 @@ -580,6 +580,33 @@ importers: rimraf: 3.0.2 tsup: 6.5.0 + integrations/resend: + specifiers: + '@react-email/render': ^0.0.3 + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 + '@types/node': '16' + '@types/react': ^18.0.26 + debug: ^4.3.4 + rimraf: ^3.0.2 + tsup: ^6.5.0 + zod: ^3.20.2 + dependencies: + '@react-email/render': 0.0.3 + debug: 4.3.4 + zod: 3.20.2 + devDependencies: + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 + '@types/node': 16.18.11 + '@types/react': 18.0.26 + rimraf: 3.0.2 + tsup: 6.5.0 + integrations/slack: specifiers: '@trigger.dev/integration-sdk': workspace:* From fad8f44ad19f9f3f4a36d6740ec7ac078b03323f Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 14:05:23 +0000 Subject: [PATCH 31/71] =?UTF-8?q?Breakout=20shopify=20into=20it=E2=80=99s?= =?UTF-8?q?=20own=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../performIntegrationRequest.server.ts | 3 +- apps/webapp/remix.config.js | 2 + apps/webapp/tsconfig.json | 4 +- config-packages/tsconfig/examples.json | 6 + examples/shopify/package.json | 4 +- examples/shopify/src/index.ts | 2 +- integrations/shopify/README.md | 3 + integrations/shopify/package.json | 46 + integrations/shopify/src/index.ts | 271 +++++ integrations/shopify/src/internal.ts | 27 + integrations/shopify/src/internal/queries.ts | 313 ++++++ integrations/shopify/src/internal/requests.ts | 968 ++++++++++++++++++ integrations/shopify/src/schemas.ts | 222 ++++ integrations/shopify/tsconfig.json | 20 + integrations/shopify/tsup.config.ts | 23 + pnpm-lock.yaml | 48 +- 16 files changed, 1946 insertions(+), 16 deletions(-) create mode 100644 integrations/shopify/README.md create mode 100644 integrations/shopify/package.json create mode 100644 integrations/shopify/src/index.ts create mode 100644 integrations/shopify/src/internal.ts create mode 100644 integrations/shopify/src/internal/queries.ts create mode 100644 integrations/shopify/src/internal/requests.ts create mode 100644 integrations/shopify/src/schemas.ts create mode 100644 integrations/shopify/tsconfig.json create mode 100644 integrations/shopify/tsup.config.ts diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index cf91ff0ed44..313f6904e4c 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -4,9 +4,10 @@ import type { NormalizedResponse, PerformedRequestResponse, } from "@trigger.dev/integration-sdk"; -import { shopify } from "internal-integrations"; + import * as slack from "@trigger.dev/slack/internal"; import * as resend from "@trigger.dev/resend/internal"; +import * as shopify from "@trigger.dev/shopify/internal"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; import type { IntegrationRequest } from "~/models/integrationRequest.server"; diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index 32376cb37aa..4322b1e8c22 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -17,6 +17,7 @@ module.exports = { "@trigger.dev/github", "@trigger.dev/slack", "@trigger.dev/resend", + "@trigger.dev/shopify", "@trigger.dev/common-schemas", "@trigger.dev/sdk", "@trigger.dev/integrations", @@ -39,6 +40,7 @@ module.exports = { "../../integrations/github/src/**/*", "../../integrations/slack/src/**/*", "../../integrations/resend/src/**/*", + "../../integrations/shopify/src/**/*", ]; }, }; diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index c48723502ae..650c13c5b99 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -55,7 +55,9 @@ "@trigger.dev/slack": ["../../integrations/slack/src/index"], "@trigger.dev/slack/*": ["../../integrations/slack/src/*"], "@trigger.dev/resend": ["../../integrations/resend/src/index"], - "@trigger.dev/resend/*": ["../../integrations/resend/src/*"] + "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], + "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], + "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"] }, "noEmit": true } diff --git a/config-packages/tsconfig/examples.json b/config-packages/tsconfig/examples.json index 9b86ed09280..bcd63ce49a7 100644 --- a/config-packages/tsconfig/examples.json +++ b/config-packages/tsconfig/examples.json @@ -47,6 +47,12 @@ ], "@trigger.dev/resend/*": [ "../../integrations/resend/src/*" + ], + "@trigger.dev/shopify": [ + "../../integrations/shopify/src/index" + ], + "@trigger.dev/shopify/*": [ + "../../integrations/shopify/src/*" ] } } diff --git a/examples/shopify/package.json b/examples/shopify/package.json index 81b5b0e65d4..a86f7c768c1 100644 --- a/examples/shopify/package.json +++ b/examples/shopify/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "description": "Send a message to slack when a customer creates a new custom domain", "dependencies": { - "@trigger.dev/integrations": "workspace:*", + "@trigger.dev/shopify": "workspace:*", "@trigger.dev/sdk": "workspace:*", "zod": "^3.20.2" }, @@ -16,4 +16,4 @@ "scripts": { "dev": "tsx src/index.ts" } -} +} \ No newline at end of file diff --git a/examples/shopify/src/index.ts b/examples/shopify/src/index.ts index 2ca4e72a290..6c65010ffae 100644 --- a/examples/shopify/src/index.ts +++ b/examples/shopify/src/index.ts @@ -1,6 +1,6 @@ import { Trigger, customEvent } from "@trigger.dev/sdk"; import { z } from "zod"; -import { shopify } from "@trigger.dev/integrations"; +import * as shopify from "@trigger.dev/shopify"; const sizes = ["Small", "Medium", "Large"]; const colors = [ diff --git a/integrations/shopify/README.md b/integrations/shopify/README.md new file mode 100644 index 00000000000..57d786d72a5 --- /dev/null +++ b/integrations/shopify/README.md @@ -0,0 +1,3 @@ +# Trigger.dev Shopify integration + +View more documentation [here](https://docs.trigger.dev) diff --git a/integrations/shopify/package.json b/integrations/shopify/package.json new file mode 100644 index 00000000000..aec77cdba72 --- /dev/null +++ b/integrations/shopify/package.json @@ -0,0 +1,46 @@ +{ + "name": "@trigger.dev/shopify", + "version": "0.1.16", + "description": "The official Shopify integration for Trigger.dev", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@shopify/shopify-api": "^6.1.0", + "@trigger.dev/integration-sdk": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/tsconfig": "workspace:*", + "@types/debug": "^4.1.7", + "@types/node": "16", + "@urql/core": "^3.1.1", + "graphql": "^16.6.0", + "react": "^18.2.0", + "rimraf": "^3.0.2", + "tsup": "^6.5.0", + "urql": "^3.0.3" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:*" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "dependencies": { + "@shopify/admin-graphql-api-utilities": "^2.0.1", + "debug": "^4.3.4", + "zod": "^3.20.2" + } +} \ No newline at end of file diff --git a/integrations/shopify/src/index.ts b/integrations/shopify/src/index.ts new file mode 100644 index 00000000000..87b4e99987e --- /dev/null +++ b/integrations/shopify/src/index.ts @@ -0,0 +1,271 @@ +import { getTriggerRun } from "@trigger.dev/sdk"; +import { z } from "zod"; +import { + SearchVariantsBodySchema, + SearchVariantsSuccessResponseSchema, + CreateVariantBodySchema, + ProductVariantSchema, + GetProductBodySchema, + ProductSchema, + CreateProductBodySchema, + UpdateProductBodySchema, + AppendProductImagesBodySchema, + AppendProductImagesResponseSchema, + ListCollectionsBodySchema, + ListCollectionsResponseSchema, + ListLocationsBodySchema, + ListLocationsResponseSchema, + AddProductsToCollectionBodySchema, + AddProductsToCollectionResponseSchema, +} from "./schemas"; + +export type SearchVariantsOptions = z.infer; +export type SearchVariantsResponse = z.infer< + typeof SearchVariantsSuccessResponseSchema +>; + +export async function searchProductVariants( + key: string, + options: SearchVariantsOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error( + "Cannot call searchProductVariants outside of a trigger run" + ); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "productVariants.search", + params: options, + response: { + schema: SearchVariantsSuccessResponseSchema, + }, + }); + + return output; +} + +export type CreateVariantBody = z.infer; + +export type CreateVariantResponse = z.infer; + +export async function createProductVariant( + key: string, + options: CreateVariantBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error( + "Cannot call createProductVariant outside of a trigger run" + ); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "productVariant.create", + params: options, + response: { + schema: ProductVariantSchema, + }, + }); + + return output; +} + +export type GetProductBody = z.infer; + +export type GetProductResponse = z.infer; + +export async function getProduct( + key: string, + options: GetProductBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call getProduct outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "product.get", + params: options, + response: { + schema: ProductSchema, + }, + }); + + return output; +} + +export type CreateProductBody = z.infer; + +export type CreateProductResponse = z.infer; + +export async function createProduct( + key: string, + options: CreateProductBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call createProduct outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "product.create", + params: options, + response: { + schema: ProductSchema, + }, + }); + + return output; +} + +export type UpdateProductBody = z.infer; + +export type UpdateProductResponse = z.infer; + +export async function updateProduct( + key: string, + options: UpdateProductBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call updateProduct outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "product.update", + params: options, + response: { + schema: ProductSchema, + }, + }); + + return output; +} + +export type AppendProductImagesBody = z.infer< + typeof AppendProductImagesBodySchema +>; + +export type AppendProductImagesResponse = z.infer< + typeof AppendProductImagesResponseSchema +>; + +export async function appendProductImages( + key: string, + options: AppendProductImagesBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call appendProductImages outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "productImages.append", + params: options, + response: { + schema: AppendProductImagesResponseSchema, + }, + }); + + return output; +} + +export type ListCollectionsBody = z.infer; + +export type ListCollectionsResponse = z.infer< + typeof ListCollectionsResponseSchema +>; + +export async function listCollections( + key: string, + options: ListCollectionsBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call listCollections outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "collections.list", + params: options, + response: { + schema: ListCollectionsResponseSchema, + }, + }); + + return output; +} + +export type ListLocationsBody = z.infer; + +export type ListLocationsResponse = z.infer; + +export async function listLocations( + key: string, + options: ListLocationsBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call listLocations outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "locations.list", + params: options, + response: { + schema: ListLocationsResponseSchema, + }, + }); + + return output; +} + +export type AddProductsToCollectionBody = z.infer< + typeof AddProductsToCollectionBodySchema +>; + +export type AddProductsToCollectionResponse = z.infer< + typeof AddProductsToCollectionResponseSchema +>; + +export async function addProductsToCollection( + key: string, + options: AddProductsToCollectionBody +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error( + "Cannot call addProductsToCollection outside of a trigger run" + ); + } + + const output = await run.performRequest(key, { + service: "shopify", + endpoint: "collection.addProducts", + params: options, + response: { + schema: AddProductsToCollectionResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/shopify/src/internal.ts b/integrations/shopify/src/internal.ts new file mode 100644 index 00000000000..42f49e9f75a --- /dev/null +++ b/integrations/shopify/src/internal.ts @@ -0,0 +1,27 @@ +import { ShopifyRequestIntegration } from "./internal/requests"; + +export const requests = new ShopifyRequestIntegration(); + +export const metadata = { + name: "Shopify", + slug: "shopify", + icon: "/integrations/shopify.png", + enabledFor: "all", + authentication: { + type: "api_key", + header_name: "X-Shopify-Access-Token", + header_type: "access_token", + additionalFields: [ + { + key: "store_name", + fieldType: "text", + name: "Store name", + placeholder: "mystore", + description: `This is the name of your Shopify store`, + }, + ], + documentation: `1. Follow [this guide](https://help.shopify.com/en/manual/apps/custom-apps) to enable Custom apps`, + }, +}; + +export * as schemas from "./schemas"; diff --git a/integrations/shopify/src/internal/queries.ts b/integrations/shopify/src/internal/queries.ts new file mode 100644 index 00000000000..486e7d2d599 --- /dev/null +++ b/integrations/shopify/src/internal/queries.ts @@ -0,0 +1,313 @@ +import { gql } from "urql"; + +export const defaultFirst = 100; + +export const searchProductVariantsQuery = gql` + query SearchProductVariants($first: Int!, $filters: String) { + productVariants(first: $first, query: $filters) { + edges { + node { + id + title + createdAt + updatedAt + price + product { + id + title + } + sku + barcode + compareAtPrice + fulfillmentService { + id + } + image { + id + } + inventoryQuantity + requiresShipping + position + taxCode + taxable + weight + weightUnit + } + } + pageInfo { + hasNextPage + } + } + } +`; + +export const createProductVariantsQuery = gql` + mutation productVariantCreate($input: ProductVariantInput!) { + productVariantCreate(input: $input) { + productVariant { + id + title + createdAt + updatedAt + price + product { + id + title + } + sku + barcode + compareAtPrice + fulfillmentService { + id + } + image { + id + } + inventoryQuantity + requiresShipping + position + taxCode + taxable + weight + weightUnit + } + userErrors { + field + message + } + } + } +`; + +export const getProductQuery = gql` + query product($input: ID!) { + product(id: $input) { + id + title + status + createdAt + updatedAt + description + descriptionHtml + featuredImage { + id + } + handle + hasOnlyDefaultVariant + hasOutOfStockVariants + images(first: 10) { + edges { + node { + id + } + } + } + options { + name + } + onlineStorePreviewUrl + onlineStoreUrl + priceRange { + minVariantPrice { + amount + } + maxVariantPrice { + amount + } + } + productType + tags + totalInventory + totalVariants + tracksInventory + variants(first: 10) { + edges { + node { + id + } + } + } + } + } +`; + +export const createProductQuery = gql` + mutation productCreate($input: ProductInput!) { + productCreate(input: $input) { + product { + id + title + status + createdAt + updatedAt + description + descriptionHtml + featuredImage { + id + } + handle + hasOnlyDefaultVariant + hasOutOfStockVariants + images(first: 10) { + edges { + node { + id + } + } + } + options { + name + } + onlineStorePreviewUrl + onlineStoreUrl + priceRange { + minVariantPrice { + amount + } + maxVariantPrice { + amount + } + } + productType + tags + totalInventory + totalVariants + tracksInventory + variants(first: 10) { + edges { + node { + id + } + } + } + } + userErrors { + field + message + } + } + } +`; + +export const appendProductImagesQuery = gql` + mutation productAppendImages($input: ProductAppendImagesInput!) { + productAppendImages(input: $input) { + newImages { + id + altText + url + height + width + } + userErrors { + field + message + } + } + } +`; + +export const listCollectionsQuery = gql` + query ListCollections($first: Int!, $filters: String) { + collections(first: $first, query: $filters) { + edges { + node { + id + title + handle + updatedAt + productsCount + sortOrder + } + } + } + } +`; + +export const listLocationsQuery = gql` + query ListLocations($first: Int!) { + locations(first: $first) { + edges { + node { + id + name + isActive + } + } + } + } +`; + +export const addProductsToCollectionQuery = gql` + mutation collectionAddProducts($id: ID!, $productIds: [ID!]!) { + collectionAddProducts(id: $id, productIds: $productIds) { + collection { + id + title + productsCount + } + userErrors { + field + message + } + } + } +`; + +export const updateProductQuery = gql` + mutation productUpdate($input: ProductInput!) { + productUpdate(input: $input) { + product { + id + title + status + createdAt + updatedAt + description + descriptionHtml + featuredImage { + id + } + handle + hasOnlyDefaultVariant + hasOutOfStockVariants + images(first: 10) { + edges { + node { + id + } + } + } + options { + name + } + onlineStorePreviewUrl + onlineStoreUrl + priceRange { + minVariantPrice { + amount + } + maxVariantPrice { + amount + } + } + productType + tags + totalInventory + totalVariants + tracksInventory + variants(first: 10) { + edges { + node { + id + } + } + } + } + userErrors { + field + message + } + } + } +`; diff --git a/integrations/shopify/src/internal/requests.ts b/integrations/shopify/src/internal/requests.ts new file mode 100644 index 00000000000..e7ff68bf8ed --- /dev/null +++ b/integrations/shopify/src/internal/requests.ts @@ -0,0 +1,968 @@ +import type { + DisplayProperties, + PerformedRequestResponse, + PerformRequestOptions, + ReactNode, + RequestIntegration, +} from "@trigger.dev/integration-sdk"; +import { Client, createClient } from "@urql/core"; +import debug from "debug"; +import { + AddProductsToCollectionBodySchema, + AddProductsToCollectionResponseSchema, + AppendProductImagesBodySchema, + AppendProductImagesResponseSchema, + CreateProductBodySchema, + CreateVariantBodySchema, + GetProductBodySchema, + ListCollectionsBodySchema, + ListCollectionsResponseSchema, + ListLocationsBodySchema, + ListLocationsResponseSchema, + ProductSchema, + ProductVariantSchema, + SearchVariantsBodySchema, + SearchVariantsSuccessResponseSchema, + UpdateProductBodySchema, +} from "../schemas"; +import { + addProductsToCollectionQuery, + appendProductImagesQuery, + createProductQuery, + createProductVariantsQuery, + defaultFirst, + getProductQuery, + listCollectionsQuery, + listLocationsQuery, + searchProductVariantsQuery, + updateProductQuery, +} from "./queries"; + +const log = debug("trigger:integrations:shopify"); +export class ShopifyRequestIntegration implements RequestIntegration { + constructor( + private readonly baseUrlFormat: string = "https://{shop}.myshopify.com/admin/api/2021-07/graphql.json" + ) {} + + async perform( + options: PerformRequestOptions + ): Promise { + if (options.accessInfo.type === "oauth2") { + throw new Error("OAuth isn't currently supported for Shopify"); + } + + if (options.accessInfo.additionalFields?.store_name === undefined) { + throw new Error("Missing store_name"); + } + + const url = this.baseUrlFormat.replace( + "{shop}", + options.accessInfo.additionalFields.store_name + ); + const client = createClient({ + url, + fetchOptions: { + headers: { + "Content-Type": "application/json", + "X-Shopify-Access-Token": options.accessInfo.api_key, + }, + }, + }); + + switch (options.endpoint) { + case "product.get": { + return this.#getProduct(client, options.params); + } + case "product.create": { + return this.#createProduct(client, options.params); + } + case "product.update": { + return this.#updateProduct(client, options.params); + } + case "productVariants.search": { + return this.#searchProductVariants(client, options.params); + } + case "productVariant.create": { + return this.#createProductVariant(client, options.params); + } + case "productImages.append": { + return this.#appendProductImages(client, options.params); + } + case "collections.list": { + return this.#listCollections(client, options.params); + } + case "locations.list": { + return this.#listLocations(client, options.params); + } + case "collection.addProducts": { + return this.#addProductsToCollection(client, options.params); + } + default: { + throw new Error(`Unknown endpoint: ${options.endpoint}`); + } + } + } + + displayProperties(endpoint: string, params: any): DisplayProperties { + switch (endpoint) { + case "product.get": { + const parsedParams = GetProductBodySchema.parse(params); + return { + title: `Get product: ${parsedParams.id}`, + }; + } + case "product.create": { + const parsedParams = CreateProductBodySchema.parse(params); + return { + title: `Create product: ${parsedParams.title}`, + }; + } + case "product.update": { + const parsedParams = UpdateProductBodySchema.parse(params); + return { + title: `Update product: ${parsedParams.id}`, + }; + } + case "productVariants.search": { + const parsedParams = SearchVariantsBodySchema.parse(params); + return { + title: "Search product variants", + properties: [ + { + key: "first", + value: parsedParams.first ?? defaultFirst, + }, + ...Object.entries(parsedParams.filter ?? {}).map( + ([key, value]) => ({ + key, + value: value.join(", "), + }) + ), + ], + }; + } + case "productVariant.create": { + const parsedParams = CreateVariantBodySchema.parse(params); + return { + title: `Create product variant for ${parsedParams.productId}`, + }; + } + case "productImages.append": { + const parsedParams = AppendProductImagesBodySchema.parse(params); + return { + title: `Append product images to: ${parsedParams.id}`, + }; + } + case "collections.list": { + return { + title: "List collections", + }; + } + case "locations.list": { + return { + title: "List locations", + }; + } + case "collection.addProducts": { + const parsedParams = AddProductsToCollectionBodySchema.parse(params); + return { + title: `Add products to collection: ${parsedParams.collectionId}`, + }; + } + + default: { + return { + title: "Unknown endpoint", + }; + } + } + } + + renderComponent(input: any, output: any): ReactNode { + return null; + } + + async #searchProductVariants( + client: Client, + params: any + ): Promise { + const parsedParams = SearchVariantsBodySchema.parse(params); + log("productVariants.search %O", parsedParams); + + try { + const filters = parsedParams.filter + ? buildFilter(parsedParams.filter) + : undefined; + + const result = await client + .query(searchProductVariantsQuery, { + first: parsedParams.first ?? defaultFirst, + filters: filters, + }) + .toPromise(); + + if (result.error) { + log("productVariants.search failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("productVariants.search data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + const response = { + count: result.data.productVariants.edges.length, + productVariants: result.data.productVariants.edges.map((p: any) => ({ + id: p.node.id, + title: p.node.title, + createdAt: p.node.createdAt, + updatedAt: p.node.updatedAt, + price: p.node.price, + product: p.node.product, + sku: p.node.sku, + barcode: p.node.barcode, + compareAtPrice: p.node.compareAtPrice, + fulfillmentService: p.node.fulfillmentService, + image: p.node.image, + inventoryQuantity: p.node.inventoryQuantity, + requiresShipping: p.node.requiresShipping, + position: p.node.position, + taxCode: p.node.taxCode, + taxable: p.node.taxable, + weight: p.node.weight, + weightUnit: p.node.weightUnit, + })), + }; + + const validatedResponse = + SearchVariantsSuccessResponseSchema.parse(response); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedResponse, + context: {}, + }, + }; + + log("productVariants.search performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("productVariants.search query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #createProductVariant( + client: Client, + params: any + ): Promise { + const parsedParams = CreateVariantBodySchema.parse(params); + log("productVariant.create %O", parsedParams); + + try { + const result = await client + .mutation(createProductVariantsQuery, { + input: parsedParams, + }) + .toPromise(); + + if (result.error) { + log("productVariant.create failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("productVariant.create data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + if ( + result.data.productVariantCreate.userErrors && + result.data.productVariantCreate.userErrors.length > 0 + ) { + log("productVariant.create userErrors %O", result.data.userErrors); + return { + ok: false, + isRetryable: false, + response: { + output: result.data.productVariantCreate.userErrors, + context: {}, + }, + }; + } + + const productVariant = result.data.productVariantCreate.productVariant; + + const validatedProductVariant = + ProductVariantSchema.parse(productVariant); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedProductVariant, + context: {}, + }, + }; + + log("productVariant.create performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("productVariant.create query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #getProduct( + client: Client, + params: any + ): Promise { + const parsedParams = GetProductBodySchema.parse(params); + log("product.get %O", parsedParams); + + try { + const result = await client + .query(getProductQuery, { + input: parsedParams.id, + }) + .toPromise(); + + if (result.error) { + log("product.get failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("product.get data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + const product = { + ...result.data.product, + images: result.data.product.images?.edges?.map((e: any) => e.node), + variants: result.data.product.variants?.edges?.map((e: any) => e.node), + }; + + const validatedProduct = ProductSchema.parse(product); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedProduct, + context: {}, + }, + }; + + log("product.get performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("product.get query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #createProduct( + client: Client, + params: any + ): Promise { + const parsedParams = CreateProductBodySchema.parse(params); + log("product.create %O", parsedParams); + + try { + const result = await client + .mutation(createProductQuery, { + input: parsedParams, + }) + .toPromise(); + + if (result.error) { + log("product.create failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("product.create data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + if ( + result.data.productCreate.userErrors && + result.data.productCreate.userErrors.length > 0 + ) { + log("product.create userErrors %O", result.data.userErrors); + return { + ok: false, + isRetryable: false, + response: { + output: result.data.productCreate.userErrors, + context: {}, + }, + }; + } + + const product = { + ...result.data.productCreate.product, + images: result.data.productCreate.product.images?.edges?.map( + (e: any) => e.node + ), + variants: result.data.productCreate.product.variants?.edges?.map( + (e: any) => e.node + ), + }; + + const validatedProduct = ProductSchema.parse(product); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedProduct, + context: {}, + }, + }; + + log("product.create performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("product.create query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #updateProduct( + client: Client, + params: any + ): Promise { + const parsedParams = UpdateProductBodySchema.parse(params); + log("product.update %O", parsedParams); + + try { + const result = await client + .mutation(updateProductQuery, { + input: parsedParams, + }) + .toPromise(); + + if (result.error) { + log("product.update failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("product.update data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + if ( + result.data.productUpdate.userErrors && + result.data.productUpdate.userErrors.length > 0 + ) { + log("product.update userErrors %O", result.data.userErrors); + return { + ok: false, + isRetryable: false, + response: { + output: result.data.productUpdate.userErrors, + context: {}, + }, + }; + } + + const product = { + ...result.data.productUpdate.product, + images: result.data.productUpdate.product.images?.edges?.map( + (e: any) => e.node + ), + variants: result.data.productUpdate.product.variants?.edges?.map( + (e: any) => e.node + ), + }; + + const validatedProduct = ProductSchema.parse(product); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedProduct, + context: {}, + }, + }; + + log("product.update performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("product.update query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #appendProductImages( + client: Client, + params: any + ): Promise { + const parsedParams = AppendProductImagesBodySchema.parse(params); + log("productImages.append %O", parsedParams); + + try { + const result = await client + .mutation(appendProductImagesQuery, { + input: parsedParams, + }) + .toPromise(); + + if (result.error) { + log("productImages.append failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("productImages.append data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + const userErrors = result.data.productAppendImages.userErrors; + if (userErrors && userErrors.length > 0) { + log( + "productImages.append userErrors %O", + result.data.productAppendImages + ); + return { + ok: false, + isRetryable: false, + response: { + output: userErrors, + context: {}, + }, + }; + } + + const newImages = result.data.productAppendImages.newImages; + + const validatedProduct = + AppendProductImagesResponseSchema.parse(newImages); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedProduct, + context: {}, + }, + }; + + log("productImages.append performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("productImages.append query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #listCollections( + client: Client, + params: any + ): Promise { + const parsedParams = ListCollectionsBodySchema.parse(params); + log("collections.list %O", parsedParams); + + try { + const filters = parsedParams.filter + ? buildFilter(parsedParams.filter) + : undefined; + + const result = await client + .query(listCollectionsQuery, { + first: parsedParams.first ?? defaultFirst, + filters: filters, + }) + .toPromise(); + + if (result.error) { + log("collections.list failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("collections.list data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + const response = result.data.collections.edges.map((c: any) => ({ + id: c.node.id, + title: c.node.title, + handle: c.node.handle, + updatedAt: c.node.updatedAt, + productsCount: c.node.productsCount, + sortOrder: c.node.sortOrder, + })); + + const validatedResponse = ListCollectionsResponseSchema.parse(response); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedResponse, + context: {}, + }, + }; + + log("collections.list performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("collections.list query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #listLocations( + client: Client, + params: any + ): Promise { + const parsedParams = ListLocationsBodySchema.parse(params); + log("locations.list %O", parsedParams); + + try { + const result = await client + .query(listLocationsQuery, { + first: parsedParams.first ?? defaultFirst, + }) + .toPromise(); + + if (result.error) { + log("locations.list failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("locations.list data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + const response = result.data.locations.edges.map((c: any) => ({ + id: c.node.id, + name: c.node.name, + isActive: c.node.isActive, + })); + + const validatedResponse = ListLocationsResponseSchema.parse(response); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedResponse, + context: {}, + }, + }; + + log("locations.list performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("locations.list query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } + + async #addProductsToCollection( + client: Client, + params: any + ): Promise { + const parsedParams = AddProductsToCollectionBodySchema.parse(params); + log("collection.addProducts %O", parsedParams); + + try { + const result = await client + .mutation(addProductsToCollectionQuery, { + id: parsedParams.collectionId, + productIds: parsedParams.productIds, + }) + .toPromise(); + + if (result.error) { + log("collection.addProducts failed %O", result.error); + return { + ok: false, + isRetryable: false, + response: { + output: result.error, + context: {}, + }, + }; + } + + if (result.data === undefined) { + log("collection.addProducts data undefined %O"); + return { + ok: false, + isRetryable: false, + response: { + output: { + message: "No data returned", + }, + context: {}, + }, + }; + } + + const userErrors = result.data.collectionAddProducts.userErrors; + if (userErrors && userErrors.length > 0) { + log( + "collection.addProducts userErrors %O", + result.data.productAppendImages + ); + return { + ok: false, + isRetryable: false, + response: { + output: userErrors, + context: {}, + }, + }; + } + + const data = { collection: result.data.collectionAddProducts.collection }; + + const validatedProduct = + AddProductsToCollectionResponseSchema.parse(data); + + const performedRequest = { + ok: true, + isRetryable: false, + response: { + output: validatedProduct, + context: {}, + }, + }; + + log("collection.addProducts performedRequest %O", performedRequest); + + return performedRequest; + } catch (error) { + log("collection.addProducts query error %O", error); + return { + ok: false, + isRetryable: false, + response: { + output: error, + context: {}, + }, + }; + } + } +} + +function buildFilter(filter: Record): string { + let filterQueries: string[] = []; + + for (const [key, values] of Object.entries(filter)) { + filterQueries.push( + `(${values + .map((value) => `${titleCaseToSnakeCase(key)}:${value}`) + .join(" OR ")})` + ); + } + + return filterQueries.join(" AND "); +} + +function titleCaseToSnakeCase(input: string): string { + return input.replace(/([A-Z])/g, (g) => `_${g[0].toLowerCase()}`); +} diff --git a/integrations/shopify/src/schemas.ts b/integrations/shopify/src/schemas.ts new file mode 100644 index 00000000000..bfa377a44ca --- /dev/null +++ b/integrations/shopify/src/schemas.ts @@ -0,0 +1,222 @@ +import { z } from "zod"; +import { parseGid } from "@shopify/admin-graphql-api-utilities"; + +export const FirstOrLastSchema = z + .object({ + first: z.number().optional(), + }) + .default({ first: 100 }); + +const objectWithId = z.object({ + id: z.string(), +}); + +const ProductStatusSchema = z.union([ + z.literal("ACTIVE"), + z.literal("ARCHIVED"), + z.literal("DRAFT"), +]); + +export const ProductSchema = z.object({ + id: z.string(), + title: z.string(), + status: ProductStatusSchema, + createdAt: z.string().nullable(), + updatedAt: z.string().nullable(), + description: z.string().nullable(), + descriptionHtml: z.string().nullable(), + featuredImage: objectWithId.nullable(), + handle: z.string().nullable(), + hasOnlyDefaultVariant: z.boolean().nullable(), + hasOutOfStockVariants: z.boolean().nullable(), + options: z.array(z.object({ name: z.string() })).nullable(), + onlineStorePreviewUrl: z.string().nullable(), + onlineStoreUrl: z.string().nullable(), + priceRange: z + .object({ + maxVariantPrice: z.object({ amount: z.string() }), + minVariantPrice: z.object({ amount: z.string() }), + }) + .nullable(), + productType: z.string().nullable(), + tags: z.array(z.string()).nullable(), + totalInventory: z.number().nullable(), + totalVariants: z.number().nullable(), + tracksInventory: z.boolean().nullable(), + variants: z.array(objectWithId).nullable(), +}); + +export const ProductVariantSchema = z.object({ + id: z.string(), + title: z.string().nullable(), + createdAt: z.string().nullable(), + updatedAt: z.string().nullable(), + price: z.string().nullable(), + product: z.object({ + id: z.string(), + title: z.string(), + }), + sku: z.string().nullable(), + barcode: z.string().nullable(), + compareAtPrice: z.string().nullable(), + fulfillmentService: objectWithId.nullable(), + image: objectWithId.nullable(), + inventoryQuantity: z.number().nullable(), + requiresShipping: z.boolean().nullable(), + position: z.number().nullable(), + taxCode: z.string().nullable(), + taxable: z.boolean().nullable(), + weight: z.number().nullable(), + weightUnit: z.string().nullable(), +}); + +export const SearchVariantsBodySchema = FirstOrLastSchema.and( + z.object({ + filter: z + .object({ + productId: z.array(z.string().transform((s) => parseGid(s))).optional(), + sku: z.array(z.string()).optional(), + }) + .optional(), + }) +); + +export const SearchVariantsSuccessResponseSchema = z.object({ + count: z.number(), + productVariants: z.array(ProductVariantSchema), +}); + +const InventoryQuantitySchema = z.object({ + availableQuantity: z.number(), + locationId: z.string(), +}); + +const InventoryItemSchema = z.object({ + cost: z.string().optional(), + tracked: z.boolean().optional(), +}); + +const InventoryPolicy = z.union([z.literal("CONTINUE"), z.literal("DENY")]); + +export const CreateVariantBodySchema = z.object({ + productId: z.string(), + options: z.array(z.string()), + price: z.string().optional(), + sku: z.string().optional(), + barcode: z.string().optional(), + compareAtPrice: z.string().optional(), + inventoryItem: InventoryItemSchema.optional(), + inventoryPolicy: InventoryPolicy.optional(), + inventoryQuantities: z.array(InventoryQuantitySchema).optional(), + requiresShipping: z.boolean().optional(), + position: z.number().optional(), + taxCode: z.string().optional(), + taxable: z.boolean().optional(), + weight: z.number().optional(), + weightUnit: z.string().optional(), +}); + +export const GetProductBodySchema = z.object({ id: z.string() }); + +export const CreateProductBodySchema = z.object({ + title: z.string(), + collectionsToJoin: z.array(z.string()).optional(), + customProductType: z.string().optional(), + descriptionHtml: z.string().optional(), + giftCard: z.boolean().optional(), + giftCardTemplateSuffix: z.string().optional(), + handle: z.string().optional(), + options: z.array(z.string()).optional(), + productType: z.string().optional(), + requiresSellingPlan: z.boolean().optional(), + seo: z + .object({ + title: z.string().optional(), + description: z.string().optional(), + }) + .optional(), + standardizedProductType: z + .object({ + productTaxonomyNodeId: z.string(), + }) + .optional(), + status: ProductStatusSchema.optional(), + tags: z.array(z.string()).optional(), + templateSuffix: z.string().optional(), + variants: z + .array(CreateVariantBodySchema.omit({ productId: true })) + .optional(), + vendor: z.string().optional(), +}); + +export const UpdateProductBodySchema = z.object({ id: z.string() }).and( + z.object({ + ...CreateProductBodySchema.shape, + title: z.string().optional(), + }) +); + +export const UpdateProductResponseSchema = ProductSchema; + +export const AppendProductImagesBodySchema = z.object({ + id: z.string(), + images: z.array( + z.object({ src: z.string(), altText: z.string().optional() }) + ), +}); + +export const ImageSchema = z.object({ + id: z.string(), + url: z.string(), + width: z.number(), + height: z.number(), + altText: z.string().optional(), +}); + +export const AppendProductImagesResponseSchema = z.array(ImageSchema); + +export const ListCollectionsBodySchema = FirstOrLastSchema.and( + z.object({ + filter: z + .object({ + title: z.array(z.string()).optional(), + collectionType: z.array(z.string()).optional(), + publishedStatus: z.array(z.string()).optional(), + publishableStatus: z.array(z.string()).optional(), + updatedAt: z.array(z.string()).optional(), + }) + .optional(), + }) +); +export const ListCollectionsResponseSchema = z.array( + z.object({ + id: z.string(), + title: z.string(), + handle: z.string(), + updatedAt: z.string(), + productsCount: z.number(), + sortOrder: z.string(), + }) +); + +export const ListLocationsBodySchema = FirstOrLastSchema; +export const ListLocationsResponseSchema = z.array( + z.object({ + id: z.string(), + name: z.string(), + isActive: z.boolean(), + }) +); + +export const AddProductsToCollectionBodySchema = z.object({ + collectionId: z.string(), + productIds: z.array(z.string()), +}); + +export const AddProductsToCollectionResponseSchema = z.object({ + collection: z.object({ + id: z.string(), + title: z.string(), + productsCount: z.number(), + }), +}); diff --git a/integrations/shopify/tsconfig.json b/integrations/shopify/tsconfig.json new file mode 100644 index 00000000000..e159cec9c07 --- /dev/null +++ b/integrations/shopify/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ] + } + }, + "exclude": ["node_modules"] +} diff --git a/integrations/shopify/tsup.config.ts b/integrations/shopify/tsup.config.ts new file mode 100644 index 00000000000..a71244deb1a --- /dev/null +++ b/integrations/shopify/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + treeshake: { + preset: "smallest", + }, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + noExternal: ["@trigger.dev/common-schemas"], + }, +]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13d29da2280..bc5f2835847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -527,15 +527,15 @@ importers: examples/shopify: specifiers: - '@trigger.dev/integrations': workspace:* '@trigger.dev/sdk': workspace:* + '@trigger.dev/shopify': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' tsx: ^3.12.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/shopify': link:../../integrations/shopify zod: 3.20.2 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig @@ -607,6 +607,41 @@ importers: rimraf: 3.0.2 tsup: 6.5.0 + integrations/shopify: + specifiers: + '@shopify/admin-graphql-api-utilities': ^2.0.1 + '@shopify/shopify-api': ^6.1.0 + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 + '@types/node': '16' + '@urql/core': ^3.1.1 + debug: ^4.3.4 + graphql: ^16.6.0 + react: ^18.2.0 + rimraf: ^3.0.2 + tsup: ^6.5.0 + urql: ^3.0.3 + zod: ^3.20.2 + dependencies: + '@shopify/admin-graphql-api-utilities': 2.0.1 + debug: 4.3.4 + zod: 3.20.2 + devDependencies: + '@shopify/shopify-api': 6.1.0 + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 + '@types/node': 16.18.11 + '@urql/core': 3.1.1_graphql@16.6.0 + graphql: 16.6.0 + react: 18.2.0 + rimraf: 3.0.2 + tsup: 6.5.0 + urql: 3.0.3_onqnqwb3ubg5opvemcqf7c2qhy + integrations/slack: specifiers: '@trigger.dev/integration-sdk': workspace:* @@ -5212,7 +5247,6 @@ packages: /@shopify/network/1.6.4: resolution: {integrity: sha512-V+//Et386LnYdtNhQ3e33AKYfU25XEt8H5XYeMqPvJZpVvC9Z1lHKQMpmM/zq13VXjPUjt9/sNxHxMP3I6cbJg==} - dev: false /@shopify/shopify-api/6.1.0: resolution: {integrity: sha512-XUyeqqRLsp8L24allGlezw2ILQ9ej9LvWS0A6GIE6k4Ku3fQDnSDIOVI2LRRkrZG92ZO3CLoM0Lblq0K9Z8RMg==} @@ -5227,7 +5261,6 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - encoding - dev: false /@sideway/address/4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} @@ -5574,7 +5607,6 @@ packages: /@types/cookiejar/2.1.2: resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} - dev: false /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} @@ -5857,13 +5889,11 @@ packages: dependencies: '@types/cookiejar': 2.1.2 '@types/node': 18.11.18 - dev: false /@types/supertest/2.0.12: resolution: {integrity: sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==} dependencies: '@types/superagent': 4.1.16 - dev: false /@types/testing-library__jest-dom/5.14.5: resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} @@ -6081,7 +6111,6 @@ packages: dependencies: graphql: 16.6.0 wonka: 6.1.2 - dev: false /@vitejs/plugin-react/2.2.0_vite@3.2.5: resolution: {integrity: sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==} @@ -11113,7 +11142,6 @@ packages: /jose/4.11.2: resolution: {integrity: sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==} - dev: false /joycon/3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -16259,7 +16287,6 @@ packages: graphql: 16.6.0 react: 18.2.0 wonka: 6.1.2 - dev: false /use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} @@ -16643,7 +16670,6 @@ packages: /wonka/6.1.2: resolution: {integrity: sha512-zNrXPMccg/7OEp9tSfFkMgTvhhowqasiSHdJ3eCZolXxVTV/aT6HUTofoZk9gwRbGoFey/Nss3JaZKUMKMbofg==} - dev: false /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} From 6c027c3c8fa0faf6f1b2e94e8d54cffd178affc9 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 15:58:25 +0000 Subject: [PATCH 32/71] Remove trigger-providers, trigger-integrations, and internal-integrations and replace with integrations-catalog --- .../app/components/CreateNewWorkflow.tsx | 4 +- .../integrations/AddApiKeyButton.tsx | 6 +- .../components/integrations/ConnectButton.tsx | 6 +- .../integrations/ConnectOAuthButton.tsx | 4 +- .../integrations/ConnectionSelector.tsx | 4 +- .../integrations/IntegrationIcon.tsx | 4 +- apps/webapp/app/models/integrations.server.ts | 17 +- .../models/workflowListPresenter.server.ts | 20 +- .../app/models/workflowRunPresenter.server.ts | 36 +- .../$organizationSlug/__org/integrations.tsx | 15 +- .../workflows/$workflowSlug.tsx | 10 +- .../workflows/$workflowSlug/runs/$runId.tsx | 71 +- apps/webapp/app/services/accessInfo.server.ts | 2 +- .../app/services/cacheService.server.ts | 2 +- apps/webapp/app/utils/integrationMetadata.ts | 8 + apps/webapp/app/utils/integrations.ts | 8 - apps/webapp/package.json | 6 +- apps/webapp/remix.config.js | 8 +- apps/webapp/tsconfig.json | 18 +- examples/playground/package.json | 3 +- integrations/github/src/internal.ts | 13 +- integrations/resend/src/internal.ts | 23 +- .../internal/{requests.tsx => requests.ts} | 61 -- integrations/resend/tsconfig.json | 1 - integrations/shopify/src/internal.ts | 13 +- integrations/shopify/src/internal/requests.ts | 5 - integrations/slack/src/internal.ts | 13 +- integrations/slack/src/internal/requests.ts | 5 - packages/integration-catalog/package.json | 16 + packages/integration-catalog/src/index.ts | 66 ++ packages/integration-catalog/tsconfig.json | 26 + packages/integration-sdk/package.json | 1 - packages/integration-sdk/src/types.ts | 33 +- packages/internal-cli/package.json | 4 +- packages/internal-cli/src/index.ts | 2 +- packages/internal-integrations/package.json | 31 - .../internal-integrations/src/accessInfo.ts | 12 - .../internal-integrations/src/github/index.ts | 219 ---- packages/internal-integrations/src/headers.ts | 9 - packages/internal-integrations/src/index.ts | 17 - .../src/resend/index.tsx | 203 ---- .../src/services/index.ts | 128 --- .../src/shopify/index.ts | 964 ------------------ .../src/shopify/queries.ts | 313 ------ .../internal-integrations/src/slack/index.ts | 3 - .../src/slack/requests.ts | 467 --------- packages/internal-integrations/src/types.ts | 89 -- packages/internal-integrations/tsconfig.json | 17 - packages/trigger-integrations/CHANGELOG.md | 139 --- packages/trigger-integrations/DEVELOPMENT.md | 11 - packages/trigger-integrations/README.md | 3 - packages/trigger-integrations/package.json | 30 - packages/trigger-integrations/src/index.ts | 6 - .../trigger-integrations/src/integration.ts | 31 - .../src/integrations/github/events.ts | 242 ----- .../src/integrations/github/index.ts | 3 - .../src/integrations/resend/index.ts | 61 -- .../src/integrations/shopify/index.ts | 276 ----- .../src/integrations/slack/events.ts | 37 - .../src/integrations/slack/index.ts | 97 -- packages/trigger-integrations/tsconfig.json | 16 - packages/trigger-integrations/tsup.config.ts | 19 - packages/trigger-providers/CHANGELOG.md | 55 - packages/trigger-providers/package.json | 39 - packages/trigger-providers/src/index.ts | 51 - .../src/providers/airtable/index.ts | 19 - .../src/providers/github/index.ts | 13 - .../github/schemas/commitComments.ts | 31 - .../src/providers/github/schemas/index.ts | 39 - .../providers/github/schemas/issueComments.ts | 91 -- .../src/providers/github/schemas/issues.ts | 311 ------ .../github/schemas/pullRequestComments.ts | 285 ------ .../github/schemas/pullRequestReviews.ts | 79 -- .../providers/github/schemas/pullRequests.ts | 401 -------- .../src/providers/github/schemas/pushes.ts | 27 - .../src/providers/github/schemas/shared.ts | 507 --------- .../src/providers/github/schemas/stars.ts | 22 - .../src/providers/resend/index.ts | 18 - .../src/providers/resend/schemas.ts | 45 - .../src/providers/shopify/index.ts | 24 - .../src/providers/shopify/schemas.ts | 222 ---- .../src/providers/slack/blocks.ts | 861 ---------------- .../src/providers/slack/index.ts | 23 - .../src/providers/slack/interactivity.ts | 215 ---- .../src/providers/slack/prodmanifest.json | 35 - .../src/providers/slack/schemas.ts | 119 --- packages/trigger-providers/src/schemaUtils.ts | 86 -- packages/trigger-providers/src/types.ts | 35 - packages/trigger-providers/tsconfig.json | 11 - packages/trigger-providers/tsup.config.ts | 28 - pnpm-lock.yaml | 146 +-- 91 files changed, 402 insertions(+), 7413 deletions(-) create mode 100644 apps/webapp/app/utils/integrationMetadata.ts delete mode 100644 apps/webapp/app/utils/integrations.ts rename integrations/resend/src/internal/{requests.tsx => requests.ts} (63%) create mode 100644 packages/integration-catalog/package.json create mode 100644 packages/integration-catalog/src/index.ts create mode 100644 packages/integration-catalog/tsconfig.json delete mode 100644 packages/internal-integrations/package.json delete mode 100644 packages/internal-integrations/src/accessInfo.ts delete mode 100644 packages/internal-integrations/src/github/index.ts delete mode 100644 packages/internal-integrations/src/headers.ts delete mode 100644 packages/internal-integrations/src/index.ts delete mode 100644 packages/internal-integrations/src/resend/index.tsx delete mode 100644 packages/internal-integrations/src/services/index.ts delete mode 100644 packages/internal-integrations/src/shopify/index.ts delete mode 100644 packages/internal-integrations/src/shopify/queries.ts delete mode 100644 packages/internal-integrations/src/slack/index.ts delete mode 100644 packages/internal-integrations/src/slack/requests.ts delete mode 100644 packages/internal-integrations/src/types.ts delete mode 100644 packages/internal-integrations/tsconfig.json delete mode 100644 packages/trigger-integrations/CHANGELOG.md delete mode 100644 packages/trigger-integrations/DEVELOPMENT.md delete mode 100644 packages/trigger-integrations/README.md delete mode 100644 packages/trigger-integrations/package.json delete mode 100644 packages/trigger-integrations/src/index.ts delete mode 100644 packages/trigger-integrations/src/integration.ts delete mode 100644 packages/trigger-integrations/src/integrations/github/events.ts delete mode 100644 packages/trigger-integrations/src/integrations/github/index.ts delete mode 100644 packages/trigger-integrations/src/integrations/resend/index.ts delete mode 100644 packages/trigger-integrations/src/integrations/shopify/index.ts delete mode 100644 packages/trigger-integrations/src/integrations/slack/events.ts delete mode 100644 packages/trigger-integrations/src/integrations/slack/index.ts delete mode 100644 packages/trigger-integrations/tsconfig.json delete mode 100644 packages/trigger-integrations/tsup.config.ts delete mode 100644 packages/trigger-providers/CHANGELOG.md delete mode 100644 packages/trigger-providers/package.json delete mode 100644 packages/trigger-providers/src/index.ts delete mode 100644 packages/trigger-providers/src/providers/airtable/index.ts delete mode 100644 packages/trigger-providers/src/providers/github/index.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/commitComments.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/index.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/issueComments.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/issues.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/pullRequestComments.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/pullRequestReviews.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/pullRequests.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/pushes.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/shared.ts delete mode 100644 packages/trigger-providers/src/providers/github/schemas/stars.ts delete mode 100644 packages/trigger-providers/src/providers/resend/index.ts delete mode 100644 packages/trigger-providers/src/providers/resend/schemas.ts delete mode 100644 packages/trigger-providers/src/providers/shopify/index.ts delete mode 100644 packages/trigger-providers/src/providers/shopify/schemas.ts delete mode 100644 packages/trigger-providers/src/providers/slack/blocks.ts delete mode 100644 packages/trigger-providers/src/providers/slack/index.ts delete mode 100644 packages/trigger-providers/src/providers/slack/interactivity.ts delete mode 100644 packages/trigger-providers/src/providers/slack/prodmanifest.json delete mode 100644 packages/trigger-providers/src/providers/slack/schemas.ts delete mode 100644 packages/trigger-providers/src/schemaUtils.ts delete mode 100644 packages/trigger-providers/src/types.ts delete mode 100644 packages/trigger-providers/tsconfig.json delete mode 100644 packages/trigger-providers/tsup.config.ts diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 17b28a31be3..c761efcb283 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -1,5 +1,5 @@ import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; -import { SerializableProvider } from "@trigger.dev/providers"; +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import { PopupButton } from "@typeform/embed-react"; import { ApiLogoIcon } from "~/components/code/ApiLogoIcon"; import discord from "../assets/images/discord.png"; @@ -38,7 +38,7 @@ export function CreateNewWorkflow() { export function CreateNewWorkflowNoWorkflows({ providers, }: { - providers: SerializableProvider[]; + providers: IntegrationMetadata[]; }) { return ( <> diff --git a/apps/webapp/app/components/integrations/AddApiKeyButton.tsx b/apps/webapp/app/components/integrations/AddApiKeyButton.tsx index 1dd4dac25e5..a9365900b0d 100644 --- a/apps/webapp/app/components/integrations/AddApiKeyButton.tsx +++ b/apps/webapp/app/components/integrations/AddApiKeyButton.tsx @@ -1,8 +1,8 @@ import { InformationCircleIcon } from "@heroicons/react/24/outline"; import type { APIKeyAuthentication, - SerializableProvider, -} from "@trigger.dev/providers"; + IntegrationMetadata, +} from "@trigger.dev/integration-sdk"; import { marked } from "marked"; import { Fragment, useEffect, useState } from "react"; import { useTypedFetcher } from "remix-typedjson"; @@ -26,7 +26,7 @@ export function AddApiKeyButton({ className, children, }: { - integration: SerializableProvider; + integration: IntegrationMetadata; authentication: APIKeyAuthentication; organizationId: string; sourceId?: string; diff --git a/apps/webapp/app/components/integrations/ConnectButton.tsx b/apps/webapp/app/components/integrations/ConnectButton.tsx index 911cb426d06..d8ff9021cae 100644 --- a/apps/webapp/app/components/integrations/ConnectButton.tsx +++ b/apps/webapp/app/components/integrations/ConnectButton.tsx @@ -1,4 +1,4 @@ -import type { SerializableProvider } from "@trigger.dev/providers"; +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import { AddApiKeyButton } from "./AddApiKeyButton"; import { ConnectOAuthButton } from "./ConnectOAuthButton"; import { IntegrationIcon } from "./IntegrationIcon"; @@ -13,7 +13,7 @@ export function ConnectButton({ children, className, }: { - integration: SerializableProvider; + integration: IntegrationMetadata; organizationId: string; sourceId?: string; serviceId?: string; @@ -55,7 +55,7 @@ export function BasicConnectButton({ sourceId, serviceId, }: { - integration: SerializableProvider; + integration: IntegrationMetadata; organizationId: string; sourceId?: string; serviceId?: string; diff --git a/apps/webapp/app/components/integrations/ConnectOAuthButton.tsx b/apps/webapp/app/components/integrations/ConnectOAuthButton.tsx index 23ac33a77d3..6f90b603b45 100644 --- a/apps/webapp/app/components/integrations/ConnectOAuthButton.tsx +++ b/apps/webapp/app/components/integrations/ConnectOAuthButton.tsx @@ -1,6 +1,6 @@ import Pizzly from "@nangohq/pizzly-frontend"; import { useFetcher } from "@remix-run/react"; -import type { SerializableProvider } from "@trigger.dev/providers"; +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import { useCallback, useEffect } from "react"; import invariant from "tiny-invariant"; import type { Response as CreateResponse } from "~/routes/resources/connection"; @@ -18,7 +18,7 @@ export function ConnectOAuthButton({ className, children, }: { - integration: SerializableProvider; + integration: IntegrationMetadata; organizationId: string; sourceId?: string; serviceId?: string; diff --git a/apps/webapp/app/components/integrations/ConnectionSelector.tsx b/apps/webapp/app/components/integrations/ConnectionSelector.tsx index 2d27af4aa9a..cfa9a057d8a 100644 --- a/apps/webapp/app/components/integrations/ConnectionSelector.tsx +++ b/apps/webapp/app/components/integrations/ConnectionSelector.tsx @@ -1,7 +1,7 @@ import { Popover, Transition } from "@headlessui/react"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline"; import { useFetcher } from "@remix-run/react"; -import type { SerializableProvider } from "@trigger.dev/providers"; +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import classNames from "classnames"; import { Fragment } from "react"; import type { APIConnection } from "~/models/apiConnection.server"; @@ -11,7 +11,7 @@ type Props = { type: "source" | "service"; sourceServiceId: string; organizationId: string; - integration: SerializableProvider; + integration: IntegrationMetadata; connections: Pick[]; selectedConnectionId?: string; className?: string; diff --git a/apps/webapp/app/components/integrations/IntegrationIcon.tsx b/apps/webapp/app/components/integrations/IntegrationIcon.tsx index b96af708263..0e2e968c9e3 100644 --- a/apps/webapp/app/components/integrations/IntegrationIcon.tsx +++ b/apps/webapp/app/components/integrations/IntegrationIcon.tsx @@ -1,9 +1,9 @@ -import type { SerializableProvider } from "@trigger.dev/providers"; +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; export function IntegrationIcon({ integration, }: { - integration: SerializableProvider; + integration: IntegrationMetadata; }) { return ( i.metadata); +} + +export function getIntegrationMetadata( + integrations: Array, + name: string +) { + const integration = integrations.find((i) => i.metadata.slug === name); + return integration ? integration.metadata : undefined; } diff --git a/apps/webapp/app/models/workflowListPresenter.server.ts b/apps/webapp/app/models/workflowListPresenter.server.ts index e11d798eb42..5729b3adf6d 100644 --- a/apps/webapp/app/models/workflowListPresenter.server.ts +++ b/apps/webapp/app/models/workflowListPresenter.server.ts @@ -10,8 +10,7 @@ import invariant from "tiny-invariant"; import { triggerLabel } from "~/components/triggers/triggerLabel"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; -import { getIntegration } from "~/utils/integrations"; -import { getIntegrations } from "./integrations.server"; +import { getIntegrationMetadata, getIntegrations } from "./integrations.server"; import { getRuntimeEnvironment } from "./runtimeEnvironment.server"; import type { ExternalSource, Workflow } from "./workflow.server"; @@ -68,10 +67,10 @@ export class WorkflowListPresenter { ), integrations: { source: workflow.service - ? getIntegration(integrations, workflow.service) + ? getIntegrationMetadata(integrations, workflow.service) : undefined, services: workflow.externalServices.map((service) => - getIntegration(integrations, service.service) + getIntegrationMetadata(integrations, service.service) ), }, lastRun, @@ -154,9 +153,16 @@ function triggerProperties( let displayProperties: DisplayProperties; switch (externalSource.service) { case "github": - displayProperties = github.webhooks.displayProperties( - externalSource.source - ); + if (github.internalIntegration.webhooks) { + displayProperties = + github.internalIntegration.webhooks?.displayProperties( + externalSource.source + ); + } else { + displayProperties = { + title: externalSource.service, + }; + } break; default: displayProperties = { diff --git a/apps/webapp/app/models/workflowRunPresenter.server.ts b/apps/webapp/app/models/workflowRunPresenter.server.ts index 89aa117095c..7844b19163a 100644 --- a/apps/webapp/app/models/workflowRunPresenter.server.ts +++ b/apps/webapp/app/models/workflowRunPresenter.server.ts @@ -8,16 +8,17 @@ import { TriggerMetadataSchema, WaitSchema, } from "@trigger.dev/common-schemas"; -import type { SerializableProvider } from "@trigger.dev/providers"; -import type { DisplayProperties } from "internal-integrations"; -import { integrations as internalIntegrations } from "internal-integrations"; +import type { + DisplayProperties, + InternalIntegration, +} from "@trigger.dev/integration-sdk"; +import { SendEmailBodySchema } from "@trigger.dev/resend/schemas"; import invariant from "tiny-invariant"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; import type { PrismaReturnType } from "~/utils"; import { dateDifference } from "~/utils"; -import { getIntegration } from "~/utils/integrations"; -import { getIntegrations } from "./integrations.server"; +import { getIntegrationMetadata, getIntegrations } from "./integrations.server"; import type { WorkflowRunStatus } from "./workflowRun.server"; export class WorkflowRunPresenter { @@ -59,13 +60,14 @@ export class WorkflowRunPresenter { dateDifference(workflowRun.startedAt, workflowRun.finishedAt), trigger: { ...trigger, - integration: getIntegration(integrations, trigger.service), + integration: getIntegrationMetadata(integrations, trigger.service), }, steps, error: workflowRun.error ? await ErrorSchema.parseAsync(workflowRun.error) : undefined, timedOutReason: workflowRun.timedOutReason, + integrations: integrations.map((i) => i.metadata), }; } } @@ -78,7 +80,7 @@ async function parseStep( original: NonNullable< PrismaReturnType >["tasks"][number], - integrations: SerializableProvider[] + integrations: InternalIntegration[] ) { const base = { id: original.id, @@ -150,17 +152,14 @@ async function parseStep( ); const externalService = original.integrationRequest.externalService; const integration = integrations.find( - (i) => i.slug === externalService.slug + (i) => i.metadata.slug === externalService.slug ); invariant(integration, `Integration ${externalService.slug} not found`); let displayProperties: DisplayProperties; - const internalIntegration = internalIntegrations[externalService.slug]; - const requests = internalIntegration?.requests; - - if (requests) { - displayProperties = requests.displayProperties( + if (integration.requests) { + displayProperties = integration.requests.displayProperties( original.integrationRequest.endpoint, original.integrationRequest.params ); @@ -170,6 +169,14 @@ async function parseStep( }; } + const customComponent = + integration.metadata.slug === "resend" + ? { + component: "resend" as const, + input: SendEmailBodySchema.parse(original.input), + } + : undefined; + return { ...base, type: "INTEGRATION_REQUEST" as const, @@ -184,9 +191,10 @@ async function parseStep( type: externalService.type, status: externalService.status, connection: externalService.connection, - integration, + integration: integration.metadata, }, retryCount: original.integrationRequest.retryCount, + customComponent, }; } diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/integrations.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/integrations.tsx index 596d6a123e6..6fe78e2432c 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/integrations.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/integrations.tsx @@ -1,7 +1,7 @@ import { EnvelopeIcon } from "@heroicons/react/24/outline"; import { PlusCircleIcon } from "@heroicons/react/24/solid"; import type { LoaderArgs } from "@remix-run/server-runtime"; -import type { SerializableProvider } from "@trigger.dev/providers"; +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import invariant from "tiny-invariant"; import { ApiLogoIcon } from "~/components/code/ApiLogoIcon"; @@ -15,10 +15,10 @@ import { SubTitle } from "~/components/primitives/text/SubTitle"; import { Title } from "~/components/primitives/text/Title"; import { useCurrentOrganization } from "~/hooks/useOrganizations"; import { getConnectedApiConnectionsForOrganizationSlug } from "~/models/apiConnection.server"; -import { getIntegrations } from "~/models/integrations.server"; +import { getIntegrationMetadatas } from "~/models/integrations.server"; import { requireUser } from "~/services/session.server"; import { formatDateTime } from "~/utils"; -import { getIntegration } from "~/utils/integrations"; +import { findIntegrationMetadata } from "~/utils/integrationMetadata"; export const loader = async ({ request, params }: LoaderArgs) => { const user = await requireUser(request); @@ -30,7 +30,10 @@ export const loader = async ({ request, params }: LoaderArgs) => { slug: organizationSlug, }); - return typedjson({ connections, integrations: getIntegrations(user.admin) }); + return typedjson({ + connections, + integrations: getIntegrationMetadatas(user.admin), + }); }; export default function Integrations() { @@ -56,7 +59,7 @@ export default function Integrations() {
  • { }); const externalSourceIntegration = integrations.find( - (i) => i.slug === workflow?.externalSource?.service + (i) => i.metadata.slug === workflow?.externalSource?.service ); const externalSourceSlot = workflow.externalSource && externalSourceIntegration @@ -84,14 +84,16 @@ export const loader = async ({ request, params }: LoaderArgs) => { possibleConnections: allConnections.filter( (a) => a.apiIdentifier === workflow?.externalSource?.service ), - integration: externalSourceIntegration, + integration: externalSourceIntegration.metadata, } : undefined; const connectionSlots = { source: externalSourceSlot, services: workflow.externalServices.flatMap((c) => { - const integration = integrations.find((i) => i.slug === c.service); + const integration = integrations.find( + (i) => i.metadata.slug === c.service + ); if (!integration) { return []; @@ -102,7 +104,7 @@ export const loader = async ({ request, params }: LoaderArgs) => { possibleConnections: allConnections.filter( (a) => a.apiIdentifier === c.service ), - integration, + integration: integration.metadata, }; }), }; diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx index 65b3c33ab9d..d21ceac2b52 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx @@ -17,13 +17,14 @@ import { import { useFetcher } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/server-runtime"; import type { Delay, Scheduled } from "@trigger.dev/common-schemas"; +import type { schemas as resendSchemas } from "@trigger.dev/resend/internal"; import classNames from "classnames"; import humanizeDuration from "humanize-duration"; -import { integrations } from "internal-integrations"; import type { ReactNode } from "react"; import { useEffect, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import invariant from "tiny-invariant"; +import type { z } from "zod"; import CodeBlock from "~/components/code/CodeBlock"; import { BasicConnectButton } from "~/components/integrations/ConnectButton"; import { Panel } from "~/components/layout/Panel"; @@ -677,11 +678,9 @@ function IntegrationRequestStep({ const organization = useCurrentOrganization(); invariant(organization, "Organization must be set"); - const internalIntegration = integrations[request.service.slug]; - const component = internalIntegration.requests?.renderComponent( - request.input, - request.output - ); + const component = request.customComponent + ? renderCustomComponent(request.customComponent) + : null; return ( <> @@ -913,3 +912,63 @@ const logColor: Record = { ERROR: "text-rose-400", DEBUG: "text-slate-400", } as const; + +function renderCustomComponent({ + component, + input, +}: { + component: "resend"; + input: z.infer; +}) { + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    {input.from}

    +

    {input.subject}

    +
    + + + +
    +
    + +
    +
    +
    + {input.html && ( +
    + )} + {input.text &&
    {input.text}
    } +
    +
    + ); +} +function EmailInfo({ + label, + value, +}: { + label: string; + value?: string | string[]; +}) { + if (!value) { + return null; + } + + return ( +
    +

    {label}:

    + {typeof value === "string" ? value : value.join(", ")} +
    + ); +} diff --git a/apps/webapp/app/services/accessInfo.server.ts b/apps/webapp/app/services/accessInfo.server.ts index 710e1d091da..fe3b919bebd 100644 --- a/apps/webapp/app/services/accessInfo.server.ts +++ b/apps/webapp/app/services/accessInfo.server.ts @@ -1,5 +1,5 @@ import type { APIConnection } from ".prisma/client"; -import type { AccessInfo } from "internal-integrations"; +import type { AccessInfo } from "@trigger.dev/integration-sdk"; import { apiKeyConfigSchema } from "~/models/apiConnection.server"; import { pizzly } from "./pizzly.server"; diff --git a/apps/webapp/app/services/cacheService.server.ts b/apps/webapp/app/services/cacheService.server.ts index 5e5f1a063d3..ddf51de0939 100644 --- a/apps/webapp/app/services/cacheService.server.ts +++ b/apps/webapp/app/services/cacheService.server.ts @@ -1,4 +1,4 @@ -import type { CacheService } from "internal-integrations"; +import type { CacheService } from "@trigger.dev/integration-sdk"; import { redis } from "./redis.server"; export class RedisCacheService implements CacheService { diff --git a/apps/webapp/app/utils/integrationMetadata.ts b/apps/webapp/app/utils/integrationMetadata.ts new file mode 100644 index 00000000000..83cb7f8667f --- /dev/null +++ b/apps/webapp/app/utils/integrationMetadata.ts @@ -0,0 +1,8 @@ +import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; + +export function findIntegrationMetadata( + integrations: Array, + slug: string +) { + return integrations.find((i) => i.slug === slug); +} diff --git a/apps/webapp/app/utils/integrations.ts b/apps/webapp/app/utils/integrations.ts deleted file mode 100644 index 74a0321433b..00000000000 --- a/apps/webapp/app/utils/integrations.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { SerializableProvider } from "@trigger.dev/providers"; - -export function getIntegration( - integrations: SerializableProvider[], - service?: string -) { - return integrations.find((i) => i.slug === service); -} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 8678e1abe2c..824f1e39a71 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -63,8 +63,11 @@ "@tailwindcss/forms": "^0.5.2", "@tanstack/react-table": "^8.0.0-alpha.87", "@trigger.dev/common-schemas": "workspace:*", - "@trigger.dev/providers": "workspace:*", + "integration-catalog": "workspace:*", "@trigger.dev/github": "workspace:*", + "@trigger.dev/slack": "workspace:*", + "@trigger.dev/resend": "workspace:*", + "@trigger.dev/shopify": "workspace:*", "@typeform/embed-react": "^2.14.1", "@uiw/react-codemirror": "^4.13.2", "bcryptjs": "^2.4.3", @@ -81,7 +84,6 @@ "express": "^4.18.1", "humanize-duration": "^3.27.3", "internal-bridge": "workspace:*", - "internal-integrations": "workspace:*", "internal-platform": "workspace:*", "internal-pulsar": "workspace:*", "ioredis": "^5.2.4", diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index 4322b1e8c22..8bd6bede068 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -11,9 +11,8 @@ module.exports = { "@nangohq/pizzly-node", "axios", "internal-platform", - "internal-integrations", "internal-bridge", - "@trigger.dev/providers", + "integration-catalog", "@trigger.dev/github", "@trigger.dev/slack", "@trigger.dev/resend", @@ -30,13 +29,10 @@ module.exports = { "../../packages/internal-platform/src/**/*", "../../packages/internal-bridge/src/**/*", "../../packages/common-schemas/src/**/*", - "../../packages/internal-integrations/src/**/*", - "../../packages/internal-providers/src/**/*", + "../../packages/integration-catalog/src/**/*", "../../packages/emails/src/**/*", "../../packages/internal-pulsar/src/**/*", "../../packages/internal-platform/src/**/*", - "../../packages/trigger-sdk/src/**/*", - "../../packages/trigger-integrations/src/**/*", "../../integrations/github/src/**/*", "../../integrations/slack/src/**/*", "../../integrations/resend/src/**/*", diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index 650c13c5b99..fdec63efed0 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -20,22 +20,8 @@ "~/*": ["./app/*"], "internal-platform": ["../../packages/internal-platform/src/index"], "internal-platform/*": ["../../packages/internal-platform/src/*"], - "internal-integrations": [ - "../../packages/internal-integrations/src/index" - ], - "internal-integrations/*": ["../../packages/internal-integrations/src/*"], "internal-bridge": ["../../packages/internal-bridge/src/index"], "internal-bridge/*": ["../../packages/internal-bridge/src/*"], - "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], - "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], - "@trigger.dev/integrations": [ - "../../packages/trigger-integrations/src/index" - ], - "@trigger.dev/integrations/*": [ - "../../packages/trigger-integrations/src/*" - ], - "@trigger.dev/providers": ["../../packages/trigger-providers/src/index"], - "@trigger.dev/providers/*": ["../../packages/trigger-providers/src/*"], "@trigger.dev/common-schemas": [ "../../packages/common-schemas/src/index" ], @@ -57,7 +43,9 @@ "@trigger.dev/resend": ["../../integrations/resend/src/index"], "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], - "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"] + "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"], + "integration-catalog": ["../../packages/integration-catalog/src/index"], + "integration-catalog/*": ["../../packages/integration-catalog/src/*"] }, "noEmit": true } diff --git a/examples/playground/package.json b/examples/playground/package.json index 45a44c6eb85..506e5b51b7c 100644 --- a/examples/playground/package.json +++ b/examples/playground/package.json @@ -4,7 +4,6 @@ "version": "0.0.1", "description": "A playground for testing Trigger.dev functionality", "dependencies": { - "@trigger.dev/integrations": "workspace:*", "@trigger.dev/sdk": "workspace:*", "zod": "^3.20.2" }, @@ -16,4 +15,4 @@ "scripts": { "dev": "tsx src/index.ts" } -} +} \ No newline at end of file diff --git a/integrations/github/src/internal.ts b/integrations/github/src/internal.ts index a4f3d91e9f0..5a2e30724b5 100644 --- a/integrations/github/src/internal.ts +++ b/integrations/github/src/internal.ts @@ -1,8 +1,12 @@ +import type { + IntegrationMetadata, + InternalIntegration, +} from "@trigger.dev/integration-sdk"; import { GitHubWebhookIntegration } from "./internal/webhooks"; -export const webhooks = new GitHubWebhookIntegration(); +const webhooks = new GitHubWebhookIntegration(); -export const metadata = { +const metadata: IntegrationMetadata = { name: "GitHub", slug: "github", icon: "/integrations/github.png", @@ -13,4 +17,9 @@ export const metadata = { }, }; +export const internalIntegration: InternalIntegration = { + metadata, + webhooks, +}; + export * as schemas from "./schemas"; diff --git a/integrations/resend/src/internal.ts b/integrations/resend/src/internal.ts index ee361ce6369..fdc9e9c8816 100644 --- a/integrations/resend/src/internal.ts +++ b/integrations/resend/src/internal.ts @@ -1,10 +1,12 @@ +import type { + IntegrationMetadata, + InternalIntegration, +} from "@trigger.dev/integration-sdk"; import { ResendRequestIntegration } from "./internal/requests"; -export const requests = new ResendRequestIntegration(); +const requests = new ResendRequestIntegration(); -export * as schemas from "./schemas"; - -export const metadata = { +const metadata: IntegrationMetadata = { name: "Resend", slug: "resend", icon: "/integrations/resend.png", @@ -14,8 +16,15 @@ export const metadata = { header_name: "Authorization", header_type: "access_token", documentation: `1. Login to [Resend](https://resend.com) -2. Go to the API Keys page -3. Generate a new API key -4. Paste it into the field below`, + 2. Go to the API Keys page + 3. Generate a new API key + 4. Paste it into the field below`, }, }; + +export const internalIntegration: InternalIntegration = { + metadata, + requests, +}; + +export * as schemas from "./schemas"; diff --git a/integrations/resend/src/internal/requests.tsx b/integrations/resend/src/internal/requests.ts similarity index 63% rename from integrations/resend/src/internal/requests.tsx rename to integrations/resend/src/internal/requests.ts index 125f43607e0..ccbc6db5c23 100644 --- a/integrations/resend/src/internal/requests.tsx +++ b/integrations/resend/src/internal/requests.ts @@ -12,7 +12,6 @@ import type { AccessInfo, } from "@trigger.dev/integration-sdk"; import debug from "debug"; -import React from "react"; import { SendEmailResponseSchema, SendEmailBodySchema } from "../schemas"; const log = debug("trigger:integrations:resend"); @@ -61,47 +60,6 @@ export class ResendRequestIntegration implements RequestIntegration { } } - renderComponent(input: any, output: any): React.ReactNode { - const parsedInput = SendEmailBodySchema.parse(input); - return ( -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - {parsedInput.from} -

    -

    {parsedInput.subject}

    -
    - - - -
    -
    - -
    -
    -
    - {parsedInput.html && ( -
    - )} - {parsedInput.text && ( -
    {parsedInput.text}
    - )} -
    -
    - ); - } - async #sendEmail( accessInfo: AccessInfo, params: any, @@ -182,22 +140,3 @@ export class ResendRequestIntegration implements RequestIntegration { ); } } - -function EmailInfo({ - label, - value, -}: { - label: string; - value?: string | string[]; -}) { - if (!value) { - return null; - } - - return ( -
    -

    {label}:

    - {typeof value === "string" ? value : value.join(", ")} -
    - ); -} diff --git a/integrations/resend/tsconfig.json b/integrations/resend/tsconfig.json index 3c7114830c1..e8aa1b855b8 100644 --- a/integrations/resend/tsconfig.json +++ b/integrations/resend/tsconfig.json @@ -7,7 +7,6 @@ "src/internal/internal.tsxernal.tsx" ], "compilerOptions": { - "jsx": "react", "experimentalDecorators": true, "emitDecoratorMetadata": true, "lib": ["DOM", "DOM.Iterable", "ES2019"], diff --git a/integrations/shopify/src/internal.ts b/integrations/shopify/src/internal.ts index 42f49e9f75a..87df8d5a157 100644 --- a/integrations/shopify/src/internal.ts +++ b/integrations/shopify/src/internal.ts @@ -1,8 +1,12 @@ +import type { + IntegrationMetadata, + InternalIntegration, +} from "@trigger.dev/integration-sdk"; import { ShopifyRequestIntegration } from "./internal/requests"; -export const requests = new ShopifyRequestIntegration(); +const requests = new ShopifyRequestIntegration(); -export const metadata = { +const metadata: IntegrationMetadata = { name: "Shopify", slug: "shopify", icon: "/integrations/shopify.png", @@ -24,4 +28,9 @@ export const metadata = { }, }; +export const internalIntegration: InternalIntegration = { + metadata, + requests, +}; + export * as schemas from "./schemas"; diff --git a/integrations/shopify/src/internal/requests.ts b/integrations/shopify/src/internal/requests.ts index e7ff68bf8ed..593326b8cde 100644 --- a/integrations/shopify/src/internal/requests.ts +++ b/integrations/shopify/src/internal/requests.ts @@ -2,7 +2,6 @@ import type { DisplayProperties, PerformedRequestResponse, PerformRequestOptions, - ReactNode, RequestIntegration, } from "@trigger.dev/integration-sdk"; import { Client, createClient } from "@urql/core"; @@ -178,10 +177,6 @@ export class ShopifyRequestIntegration implements RequestIntegration { } } - renderComponent(input: any, output: any): ReactNode { - return null; - } - async #searchProductVariants( client: Client, params: any diff --git a/integrations/slack/src/internal.ts b/integrations/slack/src/internal.ts index ee226571167..31150b43950 100644 --- a/integrations/slack/src/internal.ts +++ b/integrations/slack/src/internal.ts @@ -1,8 +1,12 @@ +import type { + IntegrationMetadata, + InternalIntegration, +} from "@trigger.dev/integration-sdk"; import { SlackRequestIntegration } from "./internal/requests"; -export const requests = new SlackRequestIntegration(); +const requests = new SlackRequestIntegration(); -export const metadata = { +const metadata: IntegrationMetadata = { name: "Slack", slug: "slack", icon: "/integrations/slack.png", @@ -23,4 +27,9 @@ export const metadata = { }, }; +export const internalIntegration: InternalIntegration = { + metadata, + requests, +}; + export * as schemas from "./schemas"; diff --git a/integrations/slack/src/internal/requests.ts b/integrations/slack/src/internal/requests.ts index 457e8c7f226..509bcc7430d 100644 --- a/integrations/slack/src/internal/requests.ts +++ b/integrations/slack/src/internal/requests.ts @@ -6,7 +6,6 @@ import type { PerformRequestOptions, RequestIntegration, AccessInfo, - ReactNode, } from "@trigger.dev/integration-sdk"; import debug from "debug"; import { getAccessToken } from "@trigger.dev/integration-sdk"; @@ -140,10 +139,6 @@ export class SlackRequestIntegration implements RequestIntegration { } } - renderComponent(input: any, output: any): ReactNode { - return null; - } - async #postMessage( accessInfo: AccessInfo, params: any, diff --git a/packages/integration-catalog/package.json b/packages/integration-catalog/package.json new file mode 100644 index 00000000000..5a3bb4ec1cf --- /dev/null +++ b/packages/integration-catalog/package.json @@ -0,0 +1,16 @@ +{ + "name": "integration-catalog", + "version": "0.1.16", + "description": "Catalog of all the Trigger.dev integration packages", + "devDependencies": { + "@trigger.dev/tsconfig": "workspace:*", + "@types/node": "16", + "@trigger.dev/slack": "workspace:*", + "@trigger.dev/github": "workspace:*", + "@trigger.dev/shopify": "workspace:*", + "@trigger.dev/resend": "workspace:*", + "@trigger.dev/integration-sdk": "workspace:*" + }, + "scripts": {}, + "dependencies": {} +} \ No newline at end of file diff --git a/packages/integration-catalog/src/index.ts b/packages/integration-catalog/src/index.ts new file mode 100644 index 00000000000..90a0ecb874d --- /dev/null +++ b/packages/integration-catalog/src/index.ts @@ -0,0 +1,66 @@ +import { internalIntegration as slack } from "@trigger.dev/slack/internal"; +import { internalIntegration as github } from "@trigger.dev/github/internal"; +import { internalIntegration as resend } from "@trigger.dev/resend/internal"; +import { internalIntegration as shopify } from "@trigger.dev/shopify/internal"; + +import type { InternalIntegration } from "@trigger.dev/integration-sdk"; + +export const airtable: InternalIntegration = { + metadata: { + name: "Airtable", + slug: "airtable", + icon: "/integrations/airtable.png", + enabledFor: "admins", + authentication: { + type: "oauth", + scopes: [ + "data.records:read", + "data.records:write", + "data.recordComments:read", + "data.recordComments:write", + "schema.bases:read", + "schema.bases:write", + "webhook:manage", + ], + }, + }, +}; + +export type IntegrationCatalog = { + integrations: Record; +}; + +const catalog = { + integrations: { + airtable, + github, + resend, + shopify, + slack, + }, +}; + +export function getIntegration(name: string): InternalIntegration; +export function getIntegration< + T extends keyof (typeof catalog)["integrations"] +>(name: T): InternalIntegration { + return catalog.integrations[name]; +} + +export function getIntegrations(isAdmin: boolean): Array { + const integrations = Object.values(catalog.integrations); + const found = integrations.filter((integration) => { + switch (integration.metadata.enabledFor) { + case "all": + return true; + case "admins": + return isAdmin; + case "none": + return false; + default: + return false; + } + }) as InternalIntegration[]; + + return found; +} diff --git a/packages/integration-catalog/tsconfig.json b/packages/integration-catalog/tsconfig.json new file mode 100644 index 00000000000..eb4f69841e9 --- /dev/null +++ b/packages/integration-catalog/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": ["./src/**/*.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2021"], + "paths": { + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/github": ["../../integrations/github/src/index"], + "@trigger.dev/github/*": ["../../integrations/github/src/*"], + "@trigger.dev/slack": ["../../integrations/slack/src/index"], + "@trigger.dev/slack/*": ["../../integrations/slack/src/*"], + "@trigger.dev/resend": ["../../integrations/resend/src/index"], + "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], + "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], + "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/packages/integration-sdk/package.json b/packages/integration-sdk/package.json index e65f2ea08d4..046ed28ad64 100644 --- a/packages/integration-sdk/package.json +++ b/packages/integration-sdk/package.json @@ -14,7 +14,6 @@ "@trigger.dev/tsconfig": "workspace:*", "@types/debug": "^4.1.7", "@types/node": "16", - "@types/react": "^18.0.21", "rimraf": "^3.0.2", "tsup": "^6.5.0" }, diff --git a/packages/integration-sdk/src/types.ts b/packages/integration-sdk/src/types.ts index 7ae10ae7394..2d4239df000 100644 --- a/packages/integration-sdk/src/types.ts +++ b/packages/integration-sdk/src/types.ts @@ -1,7 +1,3 @@ -import type { ReactNode } from "react"; - -export type { ReactNode }; - export type AccessInfo = | { type: "oauth2"; accessToken: string } | { @@ -67,7 +63,6 @@ export interface RequestIntegration { options: PerformRequestOptions ) => Promise; displayProperties: (endpoint: string, params: any) => DisplayProperties; - renderComponent(input: any, output: any): ReactNode; } export interface WebhookIntegration { @@ -88,6 +83,34 @@ export interface CacheService { } export type InternalIntegration = { + metadata: IntegrationMetadata; requests?: RequestIntegration; webhooks?: WebhookIntegration; }; + +export type IntegrationMetadata = { + name: string; + slug: string; + icon: string; + enabledFor: "all" | "admins" | "none"; + authentication: OAuthAuthentication | APIKeyAuthentication; +}; + +export type OAuthAuthentication = { + type: "oauth"; + scopes: string[]; +}; + +export type APIKeyAuthentication = { + type: "api_key"; + header_name: string; + header_type: "access_token" | "bearer"; + documentation: string; + additionalFields?: { + key: string; + fieldType: "text"; + name: string; + placeholder?: string; + description: string; + }[]; +}; diff --git a/packages/internal-cli/package.json b/packages/internal-cli/package.json index 46d46e4761f..b896622e6a9 100644 --- a/packages/internal-cli/package.json +++ b/packages/internal-cli/package.json @@ -10,7 +10,7 @@ "@types/node": "16", "@types/node-fetch": "2.6", "commander": "^9.4.1", - "@trigger.dev/providers": "workspace:*", + "integration-catalog": "workspace:*", "tiny-invariant": "^1.2.0", "tsx": "^3.12.0", "zod": "^3.20.2" @@ -24,4 +24,4 @@ "engines": { "node": ">=16.0.0 <17.0.0" } -} +} \ No newline at end of file diff --git a/packages/internal-cli/src/index.ts b/packages/internal-cli/src/index.ts index 80e857809f1..9f750824d0f 100644 --- a/packages/internal-cli/src/index.ts +++ b/packages/internal-cli/src/index.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import * as fs from "node:fs"; import { z } from "zod"; import invariant from "tiny-invariant"; -import { getProviders } from "@trigger.dev/providers"; +import { getIntegrations } from "integration-catalog"; import { fromIni } from "@aws-sdk/credential-providers"; import { SecretsManagerClient, diff --git a/packages/internal-integrations/package.json b/packages/internal-integrations/package.json deleted file mode 100644 index 7bb1c9b8526..00000000000 --- a/packages/internal-integrations/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "private": true, - "name": "internal-integrations", - "version": "0.0.1", - "description": "Common code used by integrations and the trigger.dev platform", - "main": "./src/index.ts", - "types": "./src/index.ts", - "devDependencies": { - "@trigger.dev/providers": "workspace:*", - "@trigger.dev/tsconfig": "workspace:*", - "@types/debug": "^4.1.7", - "@types/node": "16", - "@types/react": "^18.0.21", - "typescript": "^4.9.4" - }, - "scripts": {}, - "dependencies": { - "@octokit/webhooks": "^10.4.0", - "@shopify/admin-graphql-api-utilities": "^2.0.1", - "@shopify/shopify-api": "^6.1.0", - "@urql/core": "^3.1.1", - "debug": "^4.3.4", - "graphql": "^16.6.0", - "react": "^18.2.0", - "urql": "^3.0.3", - "zod": "^3.20.2" - }, - "engines": { - "node": ">=16.0.0 <17.0.0" - } -} diff --git a/packages/internal-integrations/src/accessInfo.ts b/packages/internal-integrations/src/accessInfo.ts deleted file mode 100644 index 295f46edf9d..00000000000 --- a/packages/internal-integrations/src/accessInfo.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AccessInfo } from "./types"; - -export function getAccessToken(accessInfo: AccessInfo): string { - switch (accessInfo.type) { - case "oauth2": { - return accessInfo.accessToken; - } - case "api_key": { - return accessInfo.api_key; - } - } -} diff --git a/packages/internal-integrations/src/github/index.ts b/packages/internal-integrations/src/github/index.ts deleted file mode 100644 index 853746135aa..00000000000 --- a/packages/internal-integrations/src/github/index.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { Webhooks } from "@octokit/webhooks"; -import { github } from "@trigger.dev/providers"; -import crypto from "crypto"; -import { z } from "zod"; -import { getAccessToken } from "../accessInfo"; -import { - DisplayProperty, - HandleWebhookOptions, - WebhookConfig, - WebhookIntegration, -} from "../types"; - -export class GitHubWebhookIntegration implements WebhookIntegration { - keyForSource(source: unknown): string { - const githubSource = parseWebhookSource(source); - - if (githubSource.subresource === "repository") { - return `repository.${githubSource.repo}.${githubSource.events.join(".")}`; - } else if (githubSource.subresource === "organization") { - return `organization.${githubSource.org}`; - } else { - throw new Error(`Unknown subresource`); - } - } - - registerWebhook(config: WebhookConfig, source: unknown) { - const githubSource = parseWebhookSource(source); - - if (githubSource.subresource === "repository") { - return registerRepositoryWebhook(config, githubSource); - } else if (githubSource.subresource === "organization") { - return registerOrganizationWebhook(config, githubSource); - } else { - throw new Error(`Unknown subresource`); - } - } - - handleWebhookRequest(options: HandleWebhookOptions) { - const deliveryId = options.request.headers["x-github-delivery"]; - const hookId = options.request.headers["x-github-hook-id"]; - - const signature = options.request.headers["x-hub-signature-256"]; - - if (options.secret && signature) { - const githubWebhooks = new Webhooks({ - secret: options.secret, - }); - - console.log( - `[delivery=${deliveryId}][hook=${hookId}] GitHubWebhookIntegration: Verifying signature ${signature}` - ); - - if (!githubWebhooks.verify(options.request.body, signature)) { - return { - status: "error" as const, - error: `GitHubWebhookIntegration: Could not verify webhook payload, invalid signature or secret. [deliveryId = ${deliveryId}]]`, - }; - } - } - - const event = options.request.headers["x-github-event"]; - - const context = omit(options.request.headers, [ - "x-github-event", - "x-github-delivery", - "x-hub-signature-256", - "x-hub-signature", - "content-type", - "content-length", - "accept", - "accept-encoding", - "x-forwarded-proto", - ]); - - const id = md5Hash([hookId, deliveryId].join("-")); - - return { - status: "ok" as const, - data: { id, payload: options.request.body, event, context }, - }; - } - - displayProperties(source: unknown) { - const githubSource = parseWebhookSource(source); - - const title = `GitHub ${githubSource.events.join(", ")}`; - let properties: DisplayProperty[] = []; - - switch (githubSource.subresource) { - case "repository": { - properties = [ - { - key: "Repo", - value: githubSource.repo, - }, - ]; - break; - } - case "organization": { - properties = [ - { - key: "Organization", - value: githubSource.org, - }, - ]; - break; - } - default: { - throw new Error(`Unknown subresource`); - } - } - - return { title, properties }; - } -} - -export const webhooks = new GitHubWebhookIntegration(); - -async function registerRepositoryWebhook( - config: WebhookConfig, - source: z.infer -) { - // Create the webhook in github - const response = await fetch( - `https://api.github.com/repos/${source.repo}/hooks`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - Authorization: `Bearer ${getAccessToken(config.accessInfo)}`, - "X-GitHub-Api-Version": "2022-11-28", - }, - body: JSON.stringify({ - name: "web", - active: true, - events: source.events, - config: { - url: config.callbackUrl, - content_type: "json", - secret: config.secret, - insecure_ssl: "0", - }, - }), - } - ); - - if (!response.ok) { - throw new Error(`Failed to register webhook: ${response.statusText}`); - } - - const webhook = await response.json(); - - return webhook; -} - -async function registerOrganizationWebhook( - config: WebhookConfig, - source: z.infer -) { - // Create the webhook in github - const response = await fetch( - `https://api.github.com/orgs/${source.org}/hooks`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/vnd.github+json", - Authorization: `Bearer ${getAccessToken(config.accessInfo)}`, - "X-GitHub-Api-Version": "2022-11-28", - }, - body: JSON.stringify({ - name: "web", - active: true, - events: source.events, - config: { - url: config.callbackUrl, - content_type: "json", - secret: config.secret, - insecure_ssl: "0", - }, - }), - } - ); - - if (!response.ok) { - throw new Error(`Failed to register webhook: ${response.statusText}`); - } - - const webhook = await response.json(); - - return webhook; -} - -function parseWebhookSource(source: unknown) { - return github.schemas.WebhookSourceSchema.parse(source); -} - -function omit, K extends keyof T>( - obj: T, - keys: K[] -): Omit { - const result: any = {}; - - for (const key of Object.keys(obj)) { - if (!keys.includes(key as K)) { - result[key] = obj[key]; - } - } - - return result; -} - -function md5Hash(str: string): string { - const hash = crypto.createHash("md5"); - hash.update(str); - - return hash.digest("hex"); -} diff --git a/packages/internal-integrations/src/headers.ts b/packages/internal-integrations/src/headers.ts deleted file mode 100644 index 000dbe530ea..00000000000 --- a/packages/internal-integrations/src/headers.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function normalizeHeaders(headers: Headers): Record { - const normalizedHeaders: Record = {}; - - headers.forEach((value, key) => { - normalizedHeaders[key.toLowerCase()] = value; - }); - - return normalizedHeaders; -} diff --git a/packages/internal-integrations/src/index.ts b/packages/internal-integrations/src/index.ts deleted file mode 100644 index df909f63a65..00000000000 --- a/packages/internal-integrations/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as github from "./github"; -export * as github from "./github"; -import * as slack from "./slack"; -export * as slack from "./slack"; -import * as shopify from "./shopify"; -export * as shopify from "./shopify"; -import * as resend from "./resend"; -import { InternalIntegration } from "./types"; -export * as resend from "./resend"; -export * from "./types"; - -export const integrations: Record = { - github, - slack, - shopify, - resend, -}; diff --git a/packages/internal-integrations/src/resend/index.tsx b/packages/internal-integrations/src/resend/index.tsx deleted file mode 100644 index 27dbd581d64..00000000000 --- a/packages/internal-integrations/src/resend/index.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { HttpEndpoint, HttpService } from "../services"; -import { - DisplayProperties, - CacheService, - PerformedRequestResponse, - PerformRequestOptions, - RequestIntegration, - AccessInfo, -} from "../types"; -import { resend } from "@trigger.dev/providers"; -import debug from "debug"; -import { getAccessToken } from "../accessInfo"; -import { SendEmailBodySchema } from "@trigger.dev/providers/providers/resend/schemas"; -import React from "react"; - -const log = debug("trigger:integrations:resend"); - -class ResendRequestIntegration implements RequestIntegration { - #sendEmailEndpoint = new HttpEndpoint< - typeof resend.schemas.SendEmailResponseSchema, - typeof SendEmailBodySchema - >({ - response: resend.schemas.SendEmailResponseSchema, - method: "POST", - path: "/email", - }); - - constructor(private readonly baseUrl: string = "https://api.resend.com") {} - - perform(options: PerformRequestOptions): Promise { - switch (options.endpoint) { - case "email.send": { - return this.#sendEmail( - options.accessInfo, - options.params, - options.cache - ); - } - default: { - throw new Error(`Unknown endpoint: ${options.endpoint}`); - } - } - } - - displayProperties(endpoint: string, params: any): DisplayProperties { - switch (endpoint) { - case "email.send": { - const parsed = resend.schemas.SendEmailBodySchema.parse(params); - return { - title: `Send email to ${ - typeof parsed.to === "string" ? parsed.to : parsed.to.join(", ") - }`, - properties: [], - }; - } - default: { - throw new Error(`Unknown endpoint: ${endpoint}`); - } - } - } - - renderComponent(input: any, output: any): React.ReactNode { - const parsedInput = resend.schemas.SendEmailBodySchema.parse(input); - return ( -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - {parsedInput.from} -

    -

    {parsedInput.subject}

    -
    - - - -
    -
    - -
    -
    -
    - {parsedInput.html && ( -
    - )} - {parsedInput.text && ( -
    {parsedInput.text}
    - )} -
    -
    - ); - } - - async #sendEmail( - accessInfo: AccessInfo, - params: any, - cache?: CacheService - ): Promise { - const parsedParams = resend.schemas.SendEmailBodySchema.parse(params); - - log("email.send %O", parsedParams); - - const accessToken = getAccessToken(accessInfo); - - const service = new HttpService({ - accessToken, - baseUrl: this.baseUrl, - }); - - const response = await service.performRequest(this.#sendEmailEndpoint, { - ...parsedParams, - }); - - if (!response.success) { - log("email.send failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: {}, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - if (response.statusCode !== 200) { - log("email.send failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - const performedRequest = { - ok: response.success, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - - log("email.send performedRequest %O", performedRequest); - - return performedRequest; - } - - #isRetryable(statusCode: number): boolean { - return ( - statusCode === 408 || - statusCode === 429 || - statusCode === 500 || - statusCode === 502 || - statusCode === 503 || - statusCode === 504 - ); - } -} - -export const requests = new ResendRequestIntegration(); - -function EmailInfo({ - label, - value, -}: { - label: string; - value?: string | string[]; -}) { - if (!value) { - return null; - } - - return ( -
    -

    {label}:

    - {typeof value === "string" ? value : value.join(", ")} -
    - ); -} diff --git a/packages/internal-integrations/src/services/index.ts b/packages/internal-integrations/src/services/index.ts deleted file mode 100644 index 512829b9465..00000000000 --- a/packages/internal-integrations/src/services/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { z } from "zod"; -import { normalizeHeaders } from "../headers"; -import debug from "debug"; - -const log = debug("trigger:integrations:services"); - -export type HttpServiceOptions = { - accessToken: string; - baseUrl: string; -}; - -export type HttpResponse< - TResponseSchema extends z.ZodTypeAny, - TErrorResponseSchema extends z.ZodTypeAny = z.AnyZodObject -> = - | { - success: true; - statusCode: number; - headers: Record; - data: z.infer; - } - | { - success: false; - statusCode: number; - headers: Record; - error?: z.infer; - }; - -export class HttpService { - constructor(private readonly options: HttpServiceOptions) {} - - async performRequest< - TResponseSchema extends z.ZodTypeAny, - TBodySchema extends z.ZodTypeAny = z.ZodUndefined - >( - endpoint: HttpEndpoint, - body?: z.infer - ): Promise> { - const url = `${this.options.baseUrl}${endpoint.options.path}`; - const method = endpoint.options.method; - const headers = { - Authorization: `Bearer ${this.options.accessToken}`, - "Content-Type": "application/json; charset=utf-8", - }; - const actualBody = body ? JSON.stringify(body) : undefined; - - const response = await fetch(url, { - method, - headers, - body: actualBody, - }); - - log( - "%s%s response %d", - this.options.baseUrl, - endpoint.options.path, - response.status - ); - - const json = await this.#safeGetJson(response); - - if (json) { - log( - "%s%s response %O", - this.options.baseUrl, - endpoint.options.path, - json - ); - - const parsedJson = endpoint.options.response.safeParse(json); - - if (parsedJson.success) { - return { - success: true, - data: parsedJson.data, - statusCode: response.status, - headers: normalizeHeaders(response.headers), - }; - } - - log( - "response json failed to parse %O, errors: %O", - parsedJson, - parsedJson.error - ); - - return { - success: false, - statusCode: response.status, - headers: normalizeHeaders(response.headers), - error: { type: "zod-parse", issues: parsedJson.error.issues }, - }; - } - - return { - success: false, - statusCode: response.status, - headers: normalizeHeaders(response.headers), - }; - } - - #safeGetJson = async (response: Response) => { - try { - return await response.json(); - } catch (error) { - return undefined; - } - }; -} - -export type HttpEndpointOptions< - TResponseSchema extends z.ZodTypeAny, - TBodySchema extends z.ZodTypeAny = z.ZodUndefined -> = { - response: TResponseSchema; - body?: TBodySchema; - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; - path: string; -}; - -export class HttpEndpoint< - TResponseSchema extends z.ZodTypeAny, - TBodySchema extends z.ZodTypeAny = z.ZodUndefined -> { - constructor( - public options: HttpEndpointOptions - ) {} -} diff --git a/packages/internal-integrations/src/shopify/index.ts b/packages/internal-integrations/src/shopify/index.ts deleted file mode 100644 index 26272276cd3..00000000000 --- a/packages/internal-integrations/src/shopify/index.ts +++ /dev/null @@ -1,964 +0,0 @@ -import { shopify } from "@trigger.dev/providers"; -import { Client, createClient } from "@urql/core"; -import debug from "debug"; -import type { ReactNode } from "react"; -import { - DisplayProperties, - PerformedRequestResponse, - PerformRequestOptions, - RequestIntegration, -} from "../types"; -import { - addProductsToCollectionQuery, - appendProductImagesQuery, - createProductQuery, - createProductVariantsQuery, - defaultFirst, - getProductQuery, - listCollectionsQuery, - listLocationsQuery, - searchProductVariantsQuery, - updateProductQuery, -} from "./queries"; - -const log = debug("trigger:integrations:shopify"); -class ShopifyRequestIntegration implements RequestIntegration { - constructor( - private readonly baseUrlFormat: string = "https://{shop}.myshopify.com/admin/api/2021-07/graphql.json" - ) {} - - async perform( - options: PerformRequestOptions - ): Promise { - if (options.accessInfo.type === "oauth2") { - throw new Error("OAuth isn't currently supported for Shopify"); - } - - if (options.accessInfo.additionalFields?.store_name === undefined) { - throw new Error("Missing store_name"); - } - - const url = this.baseUrlFormat.replace( - "{shop}", - options.accessInfo.additionalFields.store_name - ); - const client = createClient({ - url, - fetchOptions: { - headers: { - "Content-Type": "application/json", - "X-Shopify-Access-Token": options.accessInfo.api_key, - }, - }, - }); - - switch (options.endpoint) { - case "product.get": { - return this.#getProduct(client, options.params); - } - case "product.create": { - return this.#createProduct(client, options.params); - } - case "product.update": { - return this.#updateProduct(client, options.params); - } - case "productVariants.search": { - return this.#searchProductVariants(client, options.params); - } - case "productVariant.create": { - return this.#createProductVariant(client, options.params); - } - case "productImages.append": { - return this.#appendProductImages(client, options.params); - } - case "collections.list": { - return this.#listCollections(client, options.params); - } - case "locations.list": { - return this.#listLocations(client, options.params); - } - case "collection.addProducts": { - return this.#addProductsToCollection(client, options.params); - } - default: { - throw new Error(`Unknown endpoint: ${options.endpoint}`); - } - } - } - - displayProperties(endpoint: string, params: any): DisplayProperties { - switch (endpoint) { - case "product.get": { - const parsedParams = shopify.schemas.GetProductBodySchema.parse(params); - return { - title: `Get product: ${parsedParams.id}`, - }; - } - case "product.create": { - const parsedParams = - shopify.schemas.CreateProductBodySchema.parse(params); - return { - title: `Create product: ${parsedParams.title}`, - }; - } - case "product.update": { - const parsedParams = - shopify.schemas.UpdateProductBodySchema.parse(params); - return { - title: `Update product: ${parsedParams.id}`, - }; - } - case "productVariants.search": { - const parsedParams = - shopify.schemas.SearchVariantsBodySchema.parse(params); - return { - title: "Search product variants", - properties: [ - { - key: "first", - value: parsedParams.first ?? defaultFirst, - }, - ...Object.entries(parsedParams.filter ?? {}).map( - ([key, value]) => ({ - key, - value: value.join(", "), - }) - ), - ], - }; - } - case "productVariant.create": { - const parsedParams = - shopify.schemas.CreateVariantBodySchema.parse(params); - return { - title: `Create product variant for ${parsedParams.productId}`, - }; - } - case "productImages.append": { - const parsedParams = - shopify.schemas.AppendProductImagesBodySchema.parse(params); - return { - title: `Append product images to: ${parsedParams.id}`, - }; - } - case "collections.list": { - return { - title: "List collections", - }; - } - case "locations.list": { - return { - title: "List locations", - }; - } - case "collection.addProducts": { - const parsedParams = - shopify.schemas.AddProductsToCollectionBodySchema.parse(params); - return { - title: `Add products to collection: ${parsedParams.collectionId}`, - }; - } - - default: { - return { - title: "Unknown endpoint", - }; - } - } - } - - renderComponent(input: any, output: any): ReactNode { - return null; - } - - async #searchProductVariants( - client: Client, - params: any - ): Promise { - const parsedParams = shopify.schemas.SearchVariantsBodySchema.parse(params); - log("productVariants.search %O", parsedParams); - - try { - const filters = parsedParams.filter - ? buildFilter(parsedParams.filter) - : undefined; - - const result = await client - .query(searchProductVariantsQuery, { - first: parsedParams.first ?? defaultFirst, - filters: filters, - }) - .toPromise(); - - if (result.error) { - log("productVariants.search failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("productVariants.search data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - const response = { - count: result.data.productVariants.edges.length, - productVariants: result.data.productVariants.edges.map((p: any) => ({ - id: p.node.id, - title: p.node.title, - createdAt: p.node.createdAt, - updatedAt: p.node.updatedAt, - price: p.node.price, - product: p.node.product, - sku: p.node.sku, - barcode: p.node.barcode, - compareAtPrice: p.node.compareAtPrice, - fulfillmentService: p.node.fulfillmentService, - image: p.node.image, - inventoryQuantity: p.node.inventoryQuantity, - requiresShipping: p.node.requiresShipping, - position: p.node.position, - taxCode: p.node.taxCode, - taxable: p.node.taxable, - weight: p.node.weight, - weightUnit: p.node.weightUnit, - })), - }; - - const validatedResponse = - shopify.schemas.SearchVariantsSuccessResponseSchema.parse(response); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedResponse, - context: {}, - }, - }; - - log("productVariants.search performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("productVariants.search query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #createProductVariant( - client: Client, - params: any - ): Promise { - const parsedParams = shopify.schemas.CreateVariantBodySchema.parse(params); - log("productVariant.create %O", parsedParams); - - try { - const result = await client - .mutation(createProductVariantsQuery, { - input: parsedParams, - }) - .toPromise(); - - if (result.error) { - log("productVariant.create failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("productVariant.create data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - if ( - result.data.productVariantCreate.userErrors && - result.data.productVariantCreate.userErrors.length > 0 - ) { - log("productVariant.create userErrors %O", result.data.userErrors); - return { - ok: false, - isRetryable: false, - response: { - output: result.data.productVariantCreate.userErrors, - context: {}, - }, - }; - } - - const productVariant = result.data.productVariantCreate.productVariant; - - const validatedProductVariant = - shopify.schemas.ProductVariantSchema.parse(productVariant); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedProductVariant, - context: {}, - }, - }; - - log("productVariant.create performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("productVariant.create query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #getProduct( - client: Client, - params: any - ): Promise { - const parsedParams = shopify.schemas.GetProductBodySchema.parse(params); - log("product.get %O", parsedParams); - - try { - const result = await client - .query(getProductQuery, { - input: parsedParams.id, - }) - .toPromise(); - - if (result.error) { - log("product.get failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("product.get data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - const product = { - ...result.data.product, - images: result.data.product.images?.edges?.map((e: any) => e.node), - variants: result.data.product.variants?.edges?.map((e: any) => e.node), - }; - - const validatedProduct = shopify.schemas.ProductSchema.parse(product); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedProduct, - context: {}, - }, - }; - - log("product.get performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("product.get query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #createProduct( - client: Client, - params: any - ): Promise { - const parsedParams = shopify.schemas.CreateProductBodySchema.parse(params); - log("product.create %O", parsedParams); - - try { - const result = await client - .mutation(createProductQuery, { - input: parsedParams, - }) - .toPromise(); - - if (result.error) { - log("product.create failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("product.create data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - if ( - result.data.productCreate.userErrors && - result.data.productCreate.userErrors.length > 0 - ) { - log("product.create userErrors %O", result.data.userErrors); - return { - ok: false, - isRetryable: false, - response: { - output: result.data.productCreate.userErrors, - context: {}, - }, - }; - } - - const product = { - ...result.data.productCreate.product, - images: result.data.productCreate.product.images?.edges?.map( - (e: any) => e.node - ), - variants: result.data.productCreate.product.variants?.edges?.map( - (e: any) => e.node - ), - }; - - const validatedProduct = shopify.schemas.ProductSchema.parse(product); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedProduct, - context: {}, - }, - }; - - log("product.create performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("product.create query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #updateProduct( - client: Client, - params: any - ): Promise { - const parsedParams = shopify.schemas.UpdateProductBodySchema.parse(params); - log("product.update %O", parsedParams); - - try { - const result = await client - .mutation(updateProductQuery, { - input: parsedParams, - }) - .toPromise(); - - if (result.error) { - log("product.update failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("product.update data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - if ( - result.data.productUpdate.userErrors && - result.data.productUpdate.userErrors.length > 0 - ) { - log("product.update userErrors %O", result.data.userErrors); - return { - ok: false, - isRetryable: false, - response: { - output: result.data.productUpdate.userErrors, - context: {}, - }, - }; - } - - const product = { - ...result.data.productUpdate.product, - images: result.data.productUpdate.product.images?.edges?.map( - (e: any) => e.node - ), - variants: result.data.productUpdate.product.variants?.edges?.map( - (e: any) => e.node - ), - }; - - const validatedProduct = shopify.schemas.ProductSchema.parse(product); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedProduct, - context: {}, - }, - }; - - log("product.update performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("product.update query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #appendProductImages( - client: Client, - params: any - ): Promise { - const parsedParams = - shopify.schemas.AppendProductImagesBodySchema.parse(params); - log("productImages.append %O", parsedParams); - - try { - const result = await client - .mutation(appendProductImagesQuery, { - input: parsedParams, - }) - .toPromise(); - - if (result.error) { - log("productImages.append failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("productImages.append data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - const userErrors = result.data.productAppendImages.userErrors; - if (userErrors && userErrors.length > 0) { - log( - "productImages.append userErrors %O", - result.data.productAppendImages - ); - return { - ok: false, - isRetryable: false, - response: { - output: userErrors, - context: {}, - }, - }; - } - - const newImages = result.data.productAppendImages.newImages; - - const validatedProduct = - shopify.schemas.AppendProductImagesResponseSchema.parse(newImages); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedProduct, - context: {}, - }, - }; - - log("productImages.append performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("productImages.append query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #listCollections( - client: Client, - params: any - ): Promise { - const parsedParams = - shopify.schemas.ListCollectionsBodySchema.parse(params); - log("collections.list %O", parsedParams); - - try { - const filters = parsedParams.filter - ? buildFilter(parsedParams.filter) - : undefined; - - const result = await client - .query(listCollectionsQuery, { - first: parsedParams.first ?? defaultFirst, - filters: filters, - }) - .toPromise(); - - if (result.error) { - log("collections.list failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("collections.list data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - const response = result.data.collections.edges.map((c: any) => ({ - id: c.node.id, - title: c.node.title, - handle: c.node.handle, - updatedAt: c.node.updatedAt, - productsCount: c.node.productsCount, - sortOrder: c.node.sortOrder, - })); - - const validatedResponse = - shopify.schemas.ListCollectionsResponseSchema.parse(response); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedResponse, - context: {}, - }, - }; - - log("collections.list performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("collections.list query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #listLocations( - client: Client, - params: any - ): Promise { - const parsedParams = shopify.schemas.ListLocationsBodySchema.parse(params); - log("locations.list %O", parsedParams); - - try { - const result = await client - .query(listLocationsQuery, { - first: parsedParams.first ?? defaultFirst, - }) - .toPromise(); - - if (result.error) { - log("locations.list failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("locations.list data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - const response = result.data.locations.edges.map((c: any) => ({ - id: c.node.id, - name: c.node.name, - isActive: c.node.isActive, - })); - - const validatedResponse = - shopify.schemas.ListLocationsResponseSchema.parse(response); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedResponse, - context: {}, - }, - }; - - log("locations.list performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("locations.list query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } - - async #addProductsToCollection( - client: Client, - params: any - ): Promise { - const parsedParams = - shopify.schemas.AddProductsToCollectionBodySchema.parse(params); - log("collection.addProducts %O", parsedParams); - - try { - const result = await client - .mutation(addProductsToCollectionQuery, { - id: parsedParams.collectionId, - productIds: parsedParams.productIds, - }) - .toPromise(); - - if (result.error) { - log("collection.addProducts failed %O", result.error); - return { - ok: false, - isRetryable: false, - response: { - output: result.error, - context: {}, - }, - }; - } - - if (result.data === undefined) { - log("collection.addProducts data undefined %O"); - return { - ok: false, - isRetryable: false, - response: { - output: { - message: "No data returned", - }, - context: {}, - }, - }; - } - - const userErrors = result.data.collectionAddProducts.userErrors; - if (userErrors && userErrors.length > 0) { - log( - "collection.addProducts userErrors %O", - result.data.productAppendImages - ); - return { - ok: false, - isRetryable: false, - response: { - output: userErrors, - context: {}, - }, - }; - } - - const data = { collection: result.data.collectionAddProducts.collection }; - - const validatedProduct = - shopify.schemas.AddProductsToCollectionResponseSchema.parse(data); - - const performedRequest = { - ok: true, - isRetryable: false, - response: { - output: validatedProduct, - context: {}, - }, - }; - - log("collection.addProducts performedRequest %O", performedRequest); - - return performedRequest; - } catch (error) { - log("collection.addProducts query error %O", error); - return { - ok: false, - isRetryable: false, - response: { - output: error, - context: {}, - }, - }; - } - } -} - -export const requests = new ShopifyRequestIntegration(); - -function buildFilter(filter: Record): string { - let filterQueries: string[] = []; - - for (const [key, values] of Object.entries(filter)) { - filterQueries.push( - `(${values - .map((value) => `${titleCaseToSnakeCase(key)}:${value}`) - .join(" OR ")})` - ); - } - - return filterQueries.join(" AND "); -} - -function titleCaseToSnakeCase(input: string): string { - return input.replace(/([A-Z])/g, (g) => `_${g[0].toLowerCase()}`); -} diff --git a/packages/internal-integrations/src/shopify/queries.ts b/packages/internal-integrations/src/shopify/queries.ts deleted file mode 100644 index 486e7d2d599..00000000000 --- a/packages/internal-integrations/src/shopify/queries.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { gql } from "urql"; - -export const defaultFirst = 100; - -export const searchProductVariantsQuery = gql` - query SearchProductVariants($first: Int!, $filters: String) { - productVariants(first: $first, query: $filters) { - edges { - node { - id - title - createdAt - updatedAt - price - product { - id - title - } - sku - barcode - compareAtPrice - fulfillmentService { - id - } - image { - id - } - inventoryQuantity - requiresShipping - position - taxCode - taxable - weight - weightUnit - } - } - pageInfo { - hasNextPage - } - } - } -`; - -export const createProductVariantsQuery = gql` - mutation productVariantCreate($input: ProductVariantInput!) { - productVariantCreate(input: $input) { - productVariant { - id - title - createdAt - updatedAt - price - product { - id - title - } - sku - barcode - compareAtPrice - fulfillmentService { - id - } - image { - id - } - inventoryQuantity - requiresShipping - position - taxCode - taxable - weight - weightUnit - } - userErrors { - field - message - } - } - } -`; - -export const getProductQuery = gql` - query product($input: ID!) { - product(id: $input) { - id - title - status - createdAt - updatedAt - description - descriptionHtml - featuredImage { - id - } - handle - hasOnlyDefaultVariant - hasOutOfStockVariants - images(first: 10) { - edges { - node { - id - } - } - } - options { - name - } - onlineStorePreviewUrl - onlineStoreUrl - priceRange { - minVariantPrice { - amount - } - maxVariantPrice { - amount - } - } - productType - tags - totalInventory - totalVariants - tracksInventory - variants(first: 10) { - edges { - node { - id - } - } - } - } - } -`; - -export const createProductQuery = gql` - mutation productCreate($input: ProductInput!) { - productCreate(input: $input) { - product { - id - title - status - createdAt - updatedAt - description - descriptionHtml - featuredImage { - id - } - handle - hasOnlyDefaultVariant - hasOutOfStockVariants - images(first: 10) { - edges { - node { - id - } - } - } - options { - name - } - onlineStorePreviewUrl - onlineStoreUrl - priceRange { - minVariantPrice { - amount - } - maxVariantPrice { - amount - } - } - productType - tags - totalInventory - totalVariants - tracksInventory - variants(first: 10) { - edges { - node { - id - } - } - } - } - userErrors { - field - message - } - } - } -`; - -export const appendProductImagesQuery = gql` - mutation productAppendImages($input: ProductAppendImagesInput!) { - productAppendImages(input: $input) { - newImages { - id - altText - url - height - width - } - userErrors { - field - message - } - } - } -`; - -export const listCollectionsQuery = gql` - query ListCollections($first: Int!, $filters: String) { - collections(first: $first, query: $filters) { - edges { - node { - id - title - handle - updatedAt - productsCount - sortOrder - } - } - } - } -`; - -export const listLocationsQuery = gql` - query ListLocations($first: Int!) { - locations(first: $first) { - edges { - node { - id - name - isActive - } - } - } - } -`; - -export const addProductsToCollectionQuery = gql` - mutation collectionAddProducts($id: ID!, $productIds: [ID!]!) { - collectionAddProducts(id: $id, productIds: $productIds) { - collection { - id - title - productsCount - } - userErrors { - field - message - } - } - } -`; - -export const updateProductQuery = gql` - mutation productUpdate($input: ProductInput!) { - productUpdate(input: $input) { - product { - id - title - status - createdAt - updatedAt - description - descriptionHtml - featuredImage { - id - } - handle - hasOnlyDefaultVariant - hasOutOfStockVariants - images(first: 10) { - edges { - node { - id - } - } - } - options { - name - } - onlineStorePreviewUrl - onlineStoreUrl - priceRange { - minVariantPrice { - amount - } - maxVariantPrice { - amount - } - } - productType - tags - totalInventory - totalVariants - tracksInventory - variants(first: 10) { - edges { - node { - id - } - } - } - } - userErrors { - field - message - } - } - } -`; diff --git a/packages/internal-integrations/src/slack/index.ts b/packages/internal-integrations/src/slack/index.ts deleted file mode 100644 index 0f933af238c..00000000000 --- a/packages/internal-integrations/src/slack/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { requests } from "./requests"; - -export { requests }; diff --git a/packages/internal-integrations/src/slack/requests.ts b/packages/internal-integrations/src/slack/requests.ts deleted file mode 100644 index 163bd77a6eb..00000000000 --- a/packages/internal-integrations/src/slack/requests.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { HttpEndpoint, HttpService } from "../services"; -import { - DisplayProperties, - CacheService, - PerformedRequestResponse, - PerformRequestOptions, - RequestIntegration, - AccessInfo, -} from "../types"; -import { slack } from "@trigger.dev/providers"; -import debug from "debug"; -import { getAccessToken } from "../accessInfo"; -import { z } from "zod"; -import type { ReactNode } from "react"; - -const log = debug("trigger:integrations:slack"); - -const SendSlackMessageRequestBodySchema = - slack.schemas.PostMessageBodySchema.extend({ - link_names: z.literal(1), - metadata: z - .object({ event_type: z.string(), event_payload: z.any() }) - .optional(), - }); - -class SlackRequestIntegration implements RequestIntegration { - #joinChannelEndpoint = new HttpEndpoint< - typeof slack.schemas.JoinConversationResponseSchema, - typeof slack.schemas.JoinConversationBodySchema - >({ - response: slack.schemas.JoinConversationResponseSchema, - method: "POST", - path: "/conversations.join", - }); - - #listConversationsEndpoint = new HttpEndpoint({ - response: slack.schemas.ListConversationsResponseSchema, - method: "GET", - path: "/conversations.list", - }); - - #postMessageEndpoint = new HttpEndpoint< - typeof slack.schemas.PostMessageResponseSchema, - typeof SendSlackMessageRequestBodySchema - >({ - response: slack.schemas.PostMessageResponseSchema, - method: "POST", - path: "/chat.postMessage", - }); - - #addReactionEndpoint = new HttpEndpoint< - typeof slack.schemas.AddReactionResponseSchema, - typeof slack.schemas.AddReactionOptionsSchema - >({ - response: slack.schemas.AddReactionResponseSchema, - method: "POST", - path: "/reactions.add", - }); - - constructor(private readonly baseUrl: string = "https://slack.com/api") {} - - perform(options: PerformRequestOptions): Promise { - switch (options.endpoint) { - case "chat.postMessage": { - return this.#postMessage( - options.accessInfo, - options.params, - options.cache, - options.metadata - ); - } - case "chat.postMessageResponse": { - return this.#postMessageResponse( - options.accessInfo, - options.params, - options.cache, - options.metadata - ); - } - case "reactions.add": { - return this.#addReaction( - options.accessInfo, - options.params, - options.cache, - options.metadata - ); - } - default: { - throw new Error(`Unknown endpoint: ${options.endpoint}`); - } - } - } - - displayProperties(endpoint: string, params: any): DisplayProperties { - switch (endpoint) { - case "chat.postMessage": { - return { - title: `Post message to ${ - "channelName" in params ? params.channelName : params.channelId - }`, - properties: [ - { - key: "Text", - value: params.text, - }, - ], - }; - } - case "chat.postMessageResponse": { - return { - title: `Post response`, - properties: [], - }; - } - case "reactions.add": { - return { - title: `Add reaction to message ${params.timestamp}`, - properties: [ - { - key: "Reaction", - value: params.name, - }, - ], - }; - } - - default: { - throw new Error(`Unknown endpoint: ${endpoint}`); - } - } - } - - renderComponent(input: any, output: any): ReactNode { - return null; - } - - async #postMessage( - accessInfo: AccessInfo, - params: any, - cache?: CacheService, - metadata?: Record - ): Promise { - const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params); - - log("chat.postMessage %O", parsedParams); - - const accessToken = getAccessToken(accessInfo); - - const service = new HttpService({ - accessToken, - baseUrl: this.baseUrl, - }); - - const channelId = await this.#findChannelId(service, params, cache); - - if (!channelId) { - return { - ok: false, - isRetryable: false, - response: { - output: { - message: `channelId not found`, - }, - context: { - statusCode: 404, - headers: {}, - }, - }, - }; - } - - log("found channelId %s", channelId); - - const response = await service.performRequest(this.#postMessageEndpoint, { - ...parsedParams, - link_names: 1, - channel: channelId, - metadata: metadata - ? { event_type: "post_message", event_payload: metadata } - : undefined, - }); - - if (!response.success) { - log("chat.postMessage failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.error, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - if (!response.data.ok && response.data.error === "not_in_channel") { - log( - "chat.postMessage failed with not_in_channel, attempting to join channel %s", - channelId - ); - - // Attempt to join the channel, and then retry the request - const joinResponse = await service.performRequest( - this.#joinChannelEndpoint, - { - channel: channelId, - } - ); - - if (joinResponse.success && joinResponse.data.ok) { - log("joined channel %s, retrying postMessage", channelId); - - return this.#postMessage(accessInfo, params); - } - } - - const ok = response.data.ok; - - const performedRequest = { - ok, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - async #addReaction( - accessInfo: AccessInfo, - params: any, - cache?: CacheService, - metadata?: Record - ): Promise { - const parsedParams = slack.schemas.AddReactionOptionsSchema.parse(params); - - log("reactions.add %O", parsedParams); - - const accessToken = getAccessToken(accessInfo); - - const service = new HttpService({ - accessToken, - baseUrl: this.baseUrl, - }); - - const channelId = await this.#findChannelId(service, parsedParams, cache); - - if (!channelId) { - return { - ok: false, - isRetryable: false, - response: { - output: { - message: `channelId not found`, - }, - context: { - statusCode: 404, - headers: {}, - }, - }, - }; - } - - log("found channelId %s", channelId); - - const response = await service.performRequest(this.#addReactionEndpoint, { - ...parsedParams, - // @ts-ignore - channel: channelId, - }); - - if (!response.success) { - log("reactions.add failed %O", response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.error, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - } - - if (!response.data.ok && response.data.error === "not_in_channel") { - log( - "reactions.add failed with not_in_channel, attempting to join channel %s", - channelId - ); - - // Attempt to join the channel, and then retry the request - const joinResponse = await service.performRequest( - this.#joinChannelEndpoint, - { - channel: channelId, - } - ); - - if (joinResponse.success && joinResponse.data.ok) { - log("joined channel %s, retrying reactions.add", channelId); - - return this.#addReaction(accessInfo, params); - } - } - - const ok = response.data.ok; - - const performedRequest = { - ok, - isRetryable: this.#isRetryable(response.statusCode), - response: { - output: response.data, - context: { - statusCode: response.statusCode, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - async #postMessageResponse( - accessInfo: AccessInfo, - params: any, - cache?: CacheService, - metadata?: Record - ): Promise { - const parsedParams = z - .object({ - message: slack.schemas.PostMessageResponseOptionsSchema, - responseUrl: z.string(), - }) - .parse(params); - - log("chat.postMessageResponse %O", parsedParams); - - const response = await fetch(parsedParams.responseUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...parsedParams.message, - metadata: metadata - ? { event_type: "post_message_response", event_payload: metadata } - : undefined, - }), - }); - - if (!response.ok) { - log("chat.postMessageResponse failed %O", response); - - const error = await safeGetJson(response); - - return { - ok: false, - isRetryable: this.#isRetryable(response.status), - response: { - output: error - ? error - : { name: `${response.status}`, message: response.statusText }, - context: { - statusCode: response.status, - headers: response.headers, - }, - }, - }; - } - - const output = await safeGetJson(response); - - const performedRequest = { - ok: response.ok, - isRetryable: this.#isRetryable(response.status), - response: { - output, - context: { - statusCode: response.status, - headers: response.headers, - }, - }, - }; - - log("chat.postMessage performedRequest %O", performedRequest); - - return performedRequest; - } - - #isRetryable(statusCode: number): boolean { - return ( - statusCode === 408 || - statusCode === 429 || - statusCode === 500 || - statusCode === 502 || - statusCode === 503 || - statusCode === 504 - ); - } - - // Will use the conversations.list API (using fetch) to find the channel ID - // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ") - async #findChannelId( - service: HttpService, - params: z.infer, - cache?: CacheService - ): Promise { - if ("channelId" in params) { - return params.channelId; - } - - if (!("channelName" in params)) { - throw new Error("Invalid params, mising channelId and channelName"); - } - - //if the channelName starts with a #, remove it - if (params.channelName.startsWith("#")) { - params.channelName = params.channelName.substring(1); - } - - const cachedChannelId = await cache?.get(params.channelName); - - if (cachedChannelId) { - return cachedChannelId; - } - - const response = await service.performRequest( - this.#listConversationsEndpoint - ); - - if (response.success && response.data.ok) { - const { channels } = response.data; - - const channelInfo = channels.find( - (c: any) => c.name === params.channelName - ); - - if (channelInfo) { - await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24); - return channelInfo.id; - } - } - - return undefined; - } -} - -export const requests = new SlackRequestIntegration(); - -function safeGetJson(response: Response): Promise { - return response.json().catch(() => null); -} diff --git a/packages/internal-integrations/src/types.ts b/packages/internal-integrations/src/types.ts deleted file mode 100644 index 68eba80be31..00000000000 --- a/packages/internal-integrations/src/types.ts +++ /dev/null @@ -1,89 +0,0 @@ -export type AccessInfo = - | { type: "oauth2"; accessToken: string } - | { - type: "api_key"; - api_key: string; - additionalFields?: Record; - }; - -export interface WebhookConfig { - accessInfo: AccessInfo; - callbackUrl: string; - secret: string; -} - -export interface NormalizedRequest { - rawBody: string; - body: any; - headers: Record; - searchParams: URLSearchParams; -} - -export interface NormalizedResponse { - output: NonNullable; - context: any; -} - -export interface HandleWebhookOptions { - request: NormalizedRequest; - secret?: string; -} - -export interface ReceivedWebhook { - id: string; - event: string; - payload: any; - timestamp?: string; - context?: any; -} - -export type PerformRequestOptions = { - accessInfo: AccessInfo; - endpoint: string; - params: any; - cache?: CacheService; - metadata?: Record; -}; - -export type DisplayProperties = { - title: string; - properties?: DisplayProperty[]; -}; - -export type DisplayProperty = { key: string; value: string | number | boolean }; - -export interface PerformedRequestResponse { - response: NormalizedResponse; - isRetryable: boolean; - ok: boolean; -} - -export interface RequestIntegration { - perform: ( - options: PerformRequestOptions - ) => Promise; - displayProperties: (endpoint: string, params: any) => DisplayProperties; - renderComponent(input: any, output: any): React.ReactNode; -} - -export interface WebhookIntegration { - keyForSource: (source: unknown) => string; - registerWebhook: (config: WebhookConfig, source: unknown) => Promise; - handleWebhookRequest: ( - options: HandleWebhookOptions - ) => - | { status: "ok"; data: ReceivedWebhook } - | { status: "ignored"; reason: string } - | { status: "error"; error: string }; - displayProperties: (source: unknown) => DisplayProperties; -} - -export interface CacheService { - get: (key: string) => Promise; - set: (key: string, value: string, ttl?: number) => Promise; -} - -export type InternalIntegration = { - requests?: RequestIntegration; - webhooks?: WebhookIntegration; -}; diff --git a/packages/internal-integrations/tsconfig.json b/packages/internal-integrations/tsconfig.json deleted file mode 100644 index 6f2f339d950..00000000000 --- a/packages/internal-integrations/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "@trigger.dev/tsconfig/node16.json", - "include": ["./src/**/*.ts", "./src/**/*.tsx"], - "compilerOptions": { - "jsx": "react", - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "baseUrl": ".", - "paths": { - "@trigger.dev/providers": ["../trigger-providers/src/index.ts"], - "@trigger.dev/providers/*": ["../trigger-providers/src/*"] - }, - "lib": ["DOM"] - }, - - "exclude": ["node_modules"] -} diff --git a/packages/trigger-integrations/CHANGELOG.md b/packages/trigger-integrations/CHANGELOG.md deleted file mode 100644 index c2c0a7e841e..00000000000 --- a/packages/trigger-integrations/CHANGELOG.md +++ /dev/null @@ -1,139 +0,0 @@ -# @trigger.dev/integrations - -## 0.1.16 - -### Patch Changes - -- Updated dependencies [5035e10] - - @trigger.dev/providers@0.1.10 - -## 0.1.15 - -### Patch Changes - -- b290410: Slack blocks support -- Updated dependencies [52d21ac] -- Updated dependencies [b290410] - - @trigger.dev/sdk@0.2.11 - - @trigger.dev/providers@0.1.9 - -## 0.1.14 - -### Patch Changes - -- 1d5b592: Don't require react in the distributed package -- Updated dependencies [1d5b592] - - @trigger.dev/providers@0.1.8 - -## 0.1.13 - -### Patch Changes - -- Updated dependencies [e37a200] -- Updated dependencies [e63d354] - - @trigger.dev/sdk@0.2.10 - -## 0.1.12 - -### Patch Changes - -- 039321f: Improved types for the Resend integration -- Updated dependencies [039321f] - - @trigger.dev/providers@0.1.7 - - @trigger.dev/sdk@0.2.9 - -## 0.1.11 - -### Patch Changes - -- ddf4255: Added support for webhookEvent trigger -- Updated dependencies [ddf4255] -- Updated dependencies [2fd9e4f] - - @trigger.dev/sdk@0.2.8 - -## 0.1.10 - -### Patch Changes - -- c570f06: Added resend.com integration -- Updated dependencies [39b167e] -- Updated dependencies [c570f06] - - @trigger.dev/sdk@0.2.7 - - @trigger.dev/providers@0.1.6 - -## 0.1.9 - -### Patch Changes - -- Updated dependencies [f350fca] -- Updated dependencies [f316c6e] -- Updated dependencies [c69c370] - - @trigger.dev/providers@0.1.5 - - @trigger.dev/sdk@0.2.6 - -## 0.1.8 - -### Patch Changes - -- 6673798: Bundling common-schemas into @trigger.dev/sdk -- Updated dependencies [6673798] - - @trigger.dev/sdk@0.2.5 - -## 0.1.7 - -### Patch Changes - -- Updated dependencies [0b17912] - - @trigger.dev/sdk@0.2.4 - -## 0.1.6 - -### Patch Changes - -- ce0d4b9: When posting a message to Slack, you must explicitly specify either channelId or channelName -- Updated dependencies [ce0d4b9] - - @trigger.dev/providers@0.1.4 - - @trigger.dev/sdk@0.2.3 - -## 0.1.5 - -### Patch Changes - -- b36f0d0: Added additional github webhook triggers, and improved the slack integration -- Updated dependencies [7f26548] -- Updated dependencies [5de2a1a] -- Updated dependencies [d3c593c] -- Updated dependencies [b36f0d0] - - @trigger.dev/sdk@0.2.2 - - @trigger.dev/providers@0.1.3 - -## 0.1.4 - -### Patch Changes - -- Updated dependencies [7d23a7b] - - @trigger.dev/sdk@0.2.1 - -## 0.1.3 - -### Patch Changes - -- Updated dependencies [8b7b8a8] - - @trigger.dev/sdk@0.2.0 - -## 0.1.2 - -### Patch Changes - -- ae042a7: Providers is now a public package: @trigger.dev/providers -- Updated dependencies [ae042a7] - - @trigger.dev/providers@0.1.2 - - @trigger.dev/sdk@0.1.2 - -## 0.1.1 - -### Patch Changes - -- bcda9c8: Initial publish of the @trigger.dev packages -- Updated dependencies [bcda9c8] - - @trigger.dev/sdk@0.1.1 diff --git a/packages/trigger-integrations/DEVELOPMENT.md b/packages/trigger-integrations/DEVELOPMENT.md deleted file mode 100644 index 9343e5a3698..00000000000 --- a/packages/trigger-integrations/DEVELOPMENT.md +++ /dev/null @@ -1,11 +0,0 @@ -## Trigger.dev Integrations Development Guide - -### JSON Schema to Zod - -If you need to convert a JSON Schema to Zod, use this handy [transform.tools utility](https://transform.tools/json-schema-to-zod) - -### Add a new provider configuration to pizzly - -```sh -PIZZLY_HOSTPORT=http://localhost:3004 npx pizzly config:create github github "scopes,here" -``` diff --git a/packages/trigger-integrations/README.md b/packages/trigger-integrations/README.md deleted file mode 100644 index fd2d4d91ed9..00000000000 --- a/packages/trigger-integrations/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Coming soon - -View the full documentation for the [Trigger SDK](https://docs.trigger.dev) diff --git a/packages/trigger-integrations/package.json b/packages/trigger-integrations/package.json deleted file mode 100644 index 96dcb5a1c1e..00000000000 --- a/packages/trigger-integrations/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@trigger.dev/integrations", - "version": "0.1.16", - "description": "trigger.dev integrations", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "publishConfig": { - "access": "public" - }, - "files": [ - "dist" - ], - "devDependencies": { - "@trigger.dev/tsconfig": "workspace:*", - "@types/node": "16", - "rimraf": "^3.0.2", - "tsup": "^6.5.0" - }, - "scripts": { - "clean": "rimraf dist", - "build": "npm run clean && npm run build:tsup", - "build:tsup": "tsup" - }, - "dependencies": { - "@react-email/render": "^0.0.3", - "@trigger.dev/providers": "workspace:*", - "@trigger.dev/sdk": "workspace:*", - "zod": "^3.20.2" - } -} diff --git a/packages/trigger-integrations/src/index.ts b/packages/trigger-integrations/src/index.ts deleted file mode 100644 index 1d1a1302f29..00000000000 --- a/packages/trigger-integrations/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as github from "./integrations/github"; -import * as resend from "./integrations/resend"; -import * as slack from "./integrations/slack"; -import * as shopify from "./integrations/shopify"; - -export { github, resend, slack, shopify }; diff --git a/packages/trigger-integrations/src/integration.ts b/packages/trigger-integrations/src/integration.ts deleted file mode 100644 index 60ee6fb4d6f..00000000000 --- a/packages/trigger-integrations/src/integration.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod"; - -type Action = { - id: string; - name: string; - description: string; - input: TInput; - output: TOutput; -}; - -type Actions = Record>; - -type Integration = { - [K in keyof TActions]: ( - data: z.infer - ) => Promise>; -}; - -export function createIntegration( - actions: TActions -): Integration { - const integration = {} as Integration; - - for (const [key, action] of Object.entries(actions)) { - integration[key as keyof typeof integration] = async (data) => { - return data; - }; - } - - return integration; -} diff --git a/packages/trigger-integrations/src/integrations/github/events.ts b/packages/trigger-integrations/src/integrations/github/events.ts deleted file mode 100644 index 1ce94e1e861..00000000000 --- a/packages/trigger-integrations/src/integrations/github/events.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { TriggerEvent } from "@trigger.dev/sdk"; -import { github } from "@trigger.dev/providers"; - -export function commitCommentEvent(params: { - repo: string; -}): TriggerEvent< - typeof github.schemas.commitComments.commitCommentEventSchema -> { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "commit_comment", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["commit_comment"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["commit_comment"], - }), - manualRegistration: false, - }, - schema: github.schemas.commitComments.commitCommentEventSchema, - }; -} - -export function issueEvent(params: { - repo: string; -}): TriggerEvent { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "issues", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["issues"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["issues"], - }), - manualRegistration: false, - }, - schema: github.schemas.issues.issuesEventSchema, - }; -} - -export function issueCommentEvent(params: { - repo: string; -}): TriggerEvent { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "issue_comment", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["issue_comment"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["issue_comment"], - }), - manualRegistration: false, - }, - schema: github.schemas.issuesComments.issueCommentEventSchema, - }; -} - -export function pullRequestEvent(params: { - repo: string; -}): TriggerEvent { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "pull_request", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["pull_request"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["pull_request"], - }), - manualRegistration: false, - }, - schema: github.schemas.pullRequest.pullRequestEventSchema, - }; -} - -export function pullRequestCommentEvent(params: { - repo: string; -}): TriggerEvent< - typeof github.schemas.pullRequestComments.pullRequestReviewCommentEventSchema -> { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "pull_request_review_comment", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["pull_request_review_comment"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["pull_request_review_comment"], - }), - manualRegistration: false, - }, - schema: - github.schemas.pullRequestComments.pullRequestReviewCommentEventSchema, - }; -} - -export function pullRequestReviewEvent(params: { - repo: string; -}): TriggerEvent< - typeof github.schemas.pullRequestReviews.pullRequestReviewEventSchema -> { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "pull_request_review", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["pull_request_review"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["pull_request_review"], - }), - manualRegistration: false, - }, - schema: github.schemas.pullRequestReviews.pullRequestReviewEventSchema, - }; -} - -export function pushEvent(params: { - repo: string; -}): TriggerEvent { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "push", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - }, - event: ["push"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["push"], - }), - manualRegistration: false, - }, - schema: github.schemas.push.pushEventSchema, - }; -} - -export function newStarEvent(params: { - repo: string; -}): TriggerEvent { - return { - metadata: { - type: "WEBHOOK", - service: "github", - name: "star", - filter: { - service: ["github"], - payload: { - repository: { - full_name: [params.repo], - }, - action: ["created"], - }, - event: ["star"], - }, - source: github.schemas.WebhookSourceSchema.parse({ - subresource: "repository", - scopes: ["repo"], - repo: params.repo, - events: ["star"], - }), - manualRegistration: false, - }, - schema: github.schemas.stars.starCreatedEventSchema, - }; -} diff --git a/packages/trigger-integrations/src/integrations/github/index.ts b/packages/trigger-integrations/src/integrations/github/index.ts deleted file mode 100644 index 324692d48bf..00000000000 --- a/packages/trigger-integrations/src/integrations/github/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as events from "./events"; - -export { events }; diff --git a/packages/trigger-integrations/src/integrations/resend/index.ts b/packages/trigger-integrations/src/integrations/resend/index.ts deleted file mode 100644 index eada14f0a21..00000000000 --- a/packages/trigger-integrations/src/integrations/resend/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { getTriggerRun } from "@trigger.dev/sdk"; -import { z } from "zod"; -import { resend } from "@trigger.dev/providers"; -import { render } from "@react-email/render"; - -export type SendEmailOptions = z.infer< - typeof resend.schemas.SendEmailOptionsSchema ->; - -export type SendEmailResponse = z.infer< - typeof resend.schemas.SendEmailSuccessResponseSchema ->; - -export async function sendEmail( - key: string, - message: SendEmailOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call sendEmail outside of a trigger run"); - } - - //if the supplied email body is react, we need to turn it into html - let html: string | undefined = undefined; - if (message.react) { - html = render(message.react, { - pretty: true, - }); - } else if (message.html) { - html = message.html; - } - - let params: any; - if (html) { - params = { - ...message, - reply_to: message.replyTo, - html, - }; - } else { - params = { - ...message, - reply_to: message.replyTo, - }; - delete params.html; - } - delete params.replyTo; - delete params.react; - - const output = await run.performRequest(key, { - service: "resend", - endpoint: "email.send", - params, - response: { - schema: resend.schemas.SendEmailSuccessResponseSchema, - }, - }); - - return output; -} diff --git a/packages/trigger-integrations/src/integrations/shopify/index.ts b/packages/trigger-integrations/src/integrations/shopify/index.ts deleted file mode 100644 index 7d2bb1a7896..00000000000 --- a/packages/trigger-integrations/src/integrations/shopify/index.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { getTriggerRun } from "@trigger.dev/sdk"; -import { z } from "zod"; -import { shopify } from "@trigger.dev/providers"; - -export type SearchVariantsOptions = z.infer< - typeof shopify.schemas.SearchVariantsBodySchema ->; -export type SearchVariantsResponse = z.infer< - typeof shopify.schemas.SearchVariantsSuccessResponseSchema ->; - -export async function searchProductVariants( - key: string, - options: SearchVariantsOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error( - "Cannot call searchProductVariants outside of a trigger run" - ); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "productVariants.search", - params: options, - response: { - schema: shopify.schemas.SearchVariantsSuccessResponseSchema, - }, - }); - - return output; -} - -export type CreateVariantBody = z.infer< - typeof shopify.schemas.CreateVariantBodySchema ->; - -export type CreateVariantResponse = z.infer< - typeof shopify.schemas.ProductVariantSchema ->; - -export async function createProductVariant( - key: string, - options: CreateVariantBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error( - "Cannot call createProductVariant outside of a trigger run" - ); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "productVariant.create", - params: options, - response: { - schema: shopify.schemas.ProductVariantSchema, - }, - }); - - return output; -} - -export type GetProductBody = z.infer< - typeof shopify.schemas.GetProductBodySchema ->; - -export type GetProductResponse = z.infer; - -export async function getProduct( - key: string, - options: GetProductBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call getProduct outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "product.get", - params: options, - response: { - schema: shopify.schemas.ProductSchema, - }, - }); - - return output; -} - -export type CreateProductBody = z.infer< - typeof shopify.schemas.CreateProductBodySchema ->; - -export type CreateProductResponse = z.infer< - typeof shopify.schemas.ProductSchema ->; - -export async function createProduct( - key: string, - options: CreateProductBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call createProduct outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "product.create", - params: options, - response: { - schema: shopify.schemas.ProductSchema, - }, - }); - - return output; -} - -export type UpdateProductBody = z.infer< - typeof shopify.schemas.UpdateProductBodySchema ->; - -export type UpdateProductResponse = z.infer< - typeof shopify.schemas.ProductSchema ->; - -export async function updateProduct( - key: string, - options: UpdateProductBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call updateProduct outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "product.update", - params: options, - response: { - schema: shopify.schemas.ProductSchema, - }, - }); - - return output; -} - -export type AppendProductImagesBody = z.infer< - typeof shopify.schemas.AppendProductImagesBodySchema ->; - -export type AppendProductImagesResponse = z.infer< - typeof shopify.schemas.AppendProductImagesResponseSchema ->; - -export async function appendProductImages( - key: string, - options: AppendProductImagesBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call appendProductImages outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "productImages.append", - params: options, - response: { - schema: shopify.schemas.AppendProductImagesResponseSchema, - }, - }); - - return output; -} - -export type ListCollectionsBody = z.infer< - typeof shopify.schemas.ListCollectionsBodySchema ->; - -export type ListCollectionsResponse = z.infer< - typeof shopify.schemas.ListCollectionsResponseSchema ->; - -export async function listCollections( - key: string, - options: ListCollectionsBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call listCollections outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "collections.list", - params: options, - response: { - schema: shopify.schemas.ListCollectionsResponseSchema, - }, - }); - - return output; -} - -export type ListLocationsBody = z.infer< - typeof shopify.schemas.ListLocationsBodySchema ->; - -export type ListLocationsResponse = z.infer< - typeof shopify.schemas.ListLocationsResponseSchema ->; - -export async function listLocations( - key: string, - options: ListLocationsBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call listLocations outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "locations.list", - params: options, - response: { - schema: shopify.schemas.ListLocationsResponseSchema, - }, - }); - - return output; -} - -export type AddProductsToCollectionBody = z.infer< - typeof shopify.schemas.AddProductsToCollectionBodySchema ->; - -export type AddProductsToCollectionResponse = z.infer< - typeof shopify.schemas.AddProductsToCollectionResponseSchema ->; - -export async function addProductsToCollection( - key: string, - options: AddProductsToCollectionBody -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error( - "Cannot call addProductsToCollection outside of a trigger run" - ); - } - - const output = await run.performRequest(key, { - service: "shopify", - endpoint: "collection.addProducts", - params: options, - response: { - schema: shopify.schemas.AddProductsToCollectionResponseSchema, - }, - }); - - return output; -} diff --git a/packages/trigger-integrations/src/integrations/slack/events.ts b/packages/trigger-integrations/src/integrations/slack/events.ts deleted file mode 100644 index 67dc5afb914..00000000000 --- a/packages/trigger-integrations/src/integrations/slack/events.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { slack } from "@trigger.dev/providers"; -import { TriggerEvent } from "@trigger.dev/sdk"; - -export function blockActionInteraction(params: { - blockId: string; - actionId?: string | string[]; -}): TriggerEvent { - const actionIds = - typeof params.actionId === "undefined" - ? [] - : Array.isArray(params.actionId) - ? params.actionId - : [params.actionId]; - - return { - metadata: { - type: "SLACK_INTERACTION", - service: "slack", - name: "block.action", - filter: { - service: ["slack"], - payload: { - actions: { - block_id: [params.blockId], - action_id: actionIds, - }, - }, - event: ["block.action"], - }, - source: { - blockId: params.blockId, - actionIds, - }, - }, - schema: slack.schemas.blockAction, - }; -} diff --git a/packages/trigger-integrations/src/integrations/slack/index.ts b/packages/trigger-integrations/src/integrations/slack/index.ts deleted file mode 100644 index fe1fd2e2005..00000000000 --- a/packages/trigger-integrations/src/integrations/slack/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { getTriggerRun } from "@trigger.dev/sdk"; -import { z } from "zod"; -import { slack } from "@trigger.dev/providers"; -import * as events from "./events"; - -export { events }; - -export type PostMessageOptions = z.infer< - typeof slack.schemas.PostMessageOptionsSchema ->; - -export type PostMessageResponse = z.infer< - typeof slack.schemas.PostMessageSuccessResponseSchema ->; - -export async function postMessage( - key: string, - message: PostMessageOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call postMessage outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "slack", - endpoint: "chat.postMessage", - params: message, - response: { - schema: slack.schemas.PostMessageSuccessResponseSchema, - }, - }); - - return output; -} - -export type PostMessageResponseOptions = z.infer< - typeof slack.schemas.PostMessageResponseOptionsSchema ->; - -export type PostMessageResponseResponse = z.infer< - typeof slack.schemas.PostMessageResponseSuccessResponseSchema ->; - -export async function postMessageResponse( - key: string, - responseUrl: string, - message: PostMessageResponseOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call postMessageResponse outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "slack", - endpoint: "chat.postMessageResponse", - params: { message, responseUrl }, - response: { - schema: slack.schemas.PostMessageResponseSuccessResponseSchema, - }, - }); - - return output; -} - -export type AddReactionOptions = z.infer< - typeof slack.schemas.AddReactionOptionsSchema ->; - -export type AddReactionResponse = z.infer< - typeof slack.schemas.AddReactionSuccessResponseSchema ->; - -export async function addReaction( - key: string, - options: AddReactionOptions -): Promise { - const run = getTriggerRun(); - - if (!run) { - throw new Error("Cannot call addReaction outside of a trigger run"); - } - - const output = await run.performRequest(key, { - service: "slack", - endpoint: "reactions.add", - params: options, - response: { - schema: slack.schemas.AddReactionSuccessResponseSchema, - }, - }); - - return output; -} diff --git a/packages/trigger-integrations/tsconfig.json b/packages/trigger-integrations/tsconfig.json deleted file mode 100644 index e809d6a9c04..00000000000 --- a/packages/trigger-integrations/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "@trigger.dev/tsconfig/node16.json", - "include": ["./src/**/*.ts", "tsup.config.ts"], - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "paths": { - "@trigger.dev/sdk/*": ["../trigger-sdk/src/*"], - "@trigger.dev/sdk": ["../trigger-sdk/src/index"], - "@trigger.dev/providers/*": ["../trigger-providers/src/*"], - "@trigger.dev/providers": ["../trigger-providers/src/index"] - }, - "lib": ["DOM", "DOM.Iterable"] - }, - "exclude": ["node_modules"] -} diff --git a/packages/trigger-integrations/tsup.config.ts b/packages/trigger-integrations/tsup.config.ts deleted file mode 100644 index 74c4f4dc148..00000000000 --- a/packages/trigger-integrations/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - name: "main", - entry: ["./src/index.ts"], - outDir: "./dist", - platform: "node", - format: ["cjs"], - legacyOutput: true, - sourcemap: true, - clean: true, - bundle: true, - splitting: false, - dts: true, - external: ["http", "https", "util", "events", "tty", "os", "timers"], - esbuildPlugins: [], - }, -]); diff --git a/packages/trigger-providers/CHANGELOG.md b/packages/trigger-providers/CHANGELOG.md deleted file mode 100644 index 5eafd88a2b5..00000000000 --- a/packages/trigger-providers/CHANGELOG.md +++ /dev/null @@ -1,55 +0,0 @@ -# @trigger.dev/providers - -## 0.1.10 - -### Patch Changes - -- 5035e10: Create SerializableProvider and omit non-serializable data from getProviders - -## 0.1.9 - -### Patch Changes - -- b290410: Slack blocks support - -## 0.1.8 - -### Patch Changes - -- 1d5b592: Don't require react in the distributed package - -## 0.1.7 - -### Patch Changes - -- 039321f: Improved types for the Resend integration - -## 0.1.6 - -### Patch Changes - -- c570f06: Added resend.com integration - -## 0.1.5 - -### Patch Changes - -- f350fca: Added new star event to the github webhook integrations - -## 0.1.4 - -### Patch Changes - -- ce0d4b9: When posting a message to Slack, you must explicitly specify either channelId or channelName - -## 0.1.3 - -### Patch Changes - -- b36f0d0: Added additional github webhook triggers, and improved the slack integration - -## 0.1.2 - -### Patch Changes - -- ae042a7: Providers is now a public package: @trigger.dev/providers diff --git a/packages/trigger-providers/package.json b/packages/trigger-providers/package.json deleted file mode 100644 index 08b75924b37..00000000000 --- a/packages/trigger-providers/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@trigger.dev/providers", - "version": "0.1.10", - "description": "trigger.dev API providers with schemas", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "publishConfig": { - "access": "public" - }, - "files": [ - "dist" - ], - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js" - }, - "./package.json": "./package.json" - }, - "devDependencies": { - "@trigger.dev/tsconfig": "workspace:*", - "@types/node": "16", - "@types/react": "^18.0.26", - "rimraf": "^3.0.2", - "tsup": "^6.5.0", - "type-fest": "^3.5.3", - "typescript": "^4.9.4" - }, - "scripts": { - "clean": "rimraf dist", - "build": "npm run clean && npm run build:tsup", - "build:tsup": "tsup" - }, - "dependencies": { - "@shopify/admin-graphql-api-utilities": "^2.0.1", - "tiny-invariant": "^1.2.0", - "zod": "^3.20.2" - } -} diff --git a/packages/trigger-providers/src/index.ts b/packages/trigger-providers/src/index.ts deleted file mode 100644 index bd4da101b18..00000000000 --- a/packages/trigger-providers/src/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { airtable } from "./providers/airtable"; -import { github } from "./providers/github"; -import { resend } from "./providers/resend"; -import { shopify } from "./providers/shopify"; -import { slack } from "./providers/slack"; -import type { Provider, SerializableProvider } from "./types"; - -export type { - APIKeyAuthentication, - OAuthAuthentication, - Provider, - SerializableProvider, -} from "./types"; -export { airtable, github, resend, slack, shopify }; - -const providerCatalog = { - providers: { airtable, github, resend, slack, shopify }, -}; - -export function getProviders(isAdmin: boolean): Array { - const providers = Object.values(providerCatalog.providers); - const foundProviders = providers.filter((provider) => { - switch (provider.enabledFor) { - case "all": - return true; - case "admins": - return isAdmin; - case "none": - return false; - default: - return false; - } - }) as Provider[]; - - return foundProviders.map((provider) => omit(provider, ["schemas"])); -} - -export function omit, K extends keyof T>( - obj: T, - keys: K[] -): Omit { - const result: any = {}; - - for (const key of Object.keys(obj)) { - if (!keys.includes(key as K)) { - result[key] = obj[key]; - } - } - - return result; -} diff --git a/packages/trigger-providers/src/providers/airtable/index.ts b/packages/trigger-providers/src/providers/airtable/index.ts deleted file mode 100644 index 5299961ed9e..00000000000 --- a/packages/trigger-providers/src/providers/airtable/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const airtable = { - name: "Airtable", - slug: "airtable", - icon: "/integrations/airtable.png", - enabledFor: "admins", - authentication: { - type: "oauth", - scopes: [ - "data.records:read", - "data.records:write", - "data.recordComments:read", - "data.recordComments:write", - "schema.bases:read", - "schema.bases:write", - "webhook:manage", - ], - }, - schemas: {}, -}; diff --git a/packages/trigger-providers/src/providers/github/index.ts b/packages/trigger-providers/src/providers/github/index.ts deleted file mode 100644 index 273cd9dc60a..00000000000 --- a/packages/trigger-providers/src/providers/github/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as schemas from "./schemas"; - -export const github = { - name: "GitHub", - slug: "github", - icon: "/integrations/github.png", - enabledFor: "all", - authentication: { - type: "oauth", - scopes: ["repo"], - }, - schemas, -}; diff --git a/packages/trigger-providers/src/providers/github/schemas/commitComments.ts b/packages/trigger-providers/src/providers/github/schemas/commitComments.ts deleted file mode 100644 index 6559778b445..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/commitComments.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod"; -import { - userSchema, - authorAssociationSchema, - repositorySchema, - installationLiteSchema, - organizationSchema, -} from "./shared"; - -export const commitCommentEventSchema = z.object({ - action: z.literal("created"), - comment: z.object({ - url: z.string(), - html_url: z.string(), - id: z.number(), - node_id: z.string(), - user: userSchema, - position: z.number().nullable(), - line: z.number().nullable(), - path: z.string().nullable(), - commit_id: z.string(), - created_at: z.string(), - updated_at: z.string(), - author_association: authorAssociationSchema, - body: z.string(), - }), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); diff --git a/packages/trigger-providers/src/providers/github/schemas/index.ts b/packages/trigger-providers/src/providers/github/schemas/index.ts deleted file mode 100644 index 081e78e720f..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { z } from "zod"; -import * as commitComments from "./commitComments"; -import * as issuesComments from "./issueComments"; -import * as issues from "./issues"; -import * as pullRequestComments from "./pullRequestComments"; -import * as pullRequestReviews from "./pullRequestReviews"; -import * as pullRequest from "./pullRequests"; -import * as push from "./pushes"; -import * as stars from "./stars"; - -export const WebhookRepoSourceSchema = z.object({ - subresource: z.literal("repository"), - scopes: z.array(z.string()), - repo: z.string(), - events: z.array(z.string()), -}); - -export const WebhookOrganizationSourceSchema = z.object({ - subresource: z.literal("organization"), - scopes: z.array(z.string()), - org: z.string(), - events: z.array(z.string()), -}); - -export const WebhookSourceSchema = z.union([ - WebhookRepoSourceSchema, - WebhookOrganizationSourceSchema, -]); - -export { - commitComments, - issues, - issuesComments, - pullRequest, - pullRequestComments, - pullRequestReviews, - push, - stars, -}; diff --git a/packages/trigger-providers/src/providers/github/schemas/issueComments.ts b/packages/trigger-providers/src/providers/github/schemas/issueComments.ts deleted file mode 100644 index 8be801aad3b..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/issueComments.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { z } from "zod"; -import { issueSchema } from "./issues"; -import { - appSchema, - authorAssociationSchema, - installationLiteSchema, - labelSchema, - organizationSchema, - reactionsSchema, - repositorySchema, - userSchema, -} from "./shared"; - -export const issueCommentSchema = z.object({ - url: z.string(), - html_url: z.string(), - issue_url: z.string(), - id: z.number(), - node_id: z.string(), - user: userSchema, - created_at: z.string(), - updated_at: z.string(), - author_association: authorAssociationSchema, - body: z.string(), - reactions: reactionsSchema, - performed_via_github_app: appSchema.nullable(), -}); - -export const issueCommentCreatedEventSchema = z.object({ - action: z.literal("created"), - issue: issueSchema.and( - z.object({ - assignee: userSchema.nullable(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - labels: z.array(labelSchema), - }) - ), - comment: issueCommentSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issueCommentDeletedEventSchema = z.object({ - action: z.literal("deleted"), - issue: issueSchema.and( - z.object({ - assignee: userSchema.nullable(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - labels: z.array(labelSchema), - }) - ), - comment: issueCommentSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issueCommentEditedEventSchema = z.object({ - action: z.literal("edited"), - changes: z.object({ - body: z - .object({ - from: z.string(), - }) - .optional(), - }), - issue: issueSchema.and( - z.object({ - assignee: userSchema.nullable(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - labels: z.array(labelSchema), - }) - ), - comment: issueCommentSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issueCommentEventSchema = z.discriminatedUnion("action", [ - issueCommentCreatedEventSchema, - issueCommentDeletedEventSchema, - issueCommentEditedEventSchema, -]); diff --git a/packages/trigger-providers/src/providers/github/schemas/issues.ts b/packages/trigger-providers/src/providers/github/schemas/issues.ts deleted file mode 100644 index 224db1a8238..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/issues.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { z } from "zod"; -import { - appSchema, - authorAssociationSchema, - installationLiteSchema, - labelSchema, - milestoneSchema, - organizationSchema, - reactionsSchema, - repositorySchema, - userSchema, -} from "./shared"; - -export const issueSchema = z.object({ - url: z.string(), - repository_url: z.string(), - labels_url: z.string(), - comments_url: z.string(), - events_url: z.string(), - html_url: z.string(), - id: z.number(), - node_id: z.string(), - number: z.number(), - title: z.string(), - user: userSchema, - labels: z.array(labelSchema).optional(), - state: z.union([z.literal("open"), z.literal("closed")]).optional(), - locked: z.boolean().optional(), - assignee: userSchema.optional().nullable(), - assignees: z.array(userSchema), - milestone: milestoneSchema.nullable(), - comments: z.number(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - author_association: authorAssociationSchema, - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), - draft: z.boolean().optional(), - performed_via_github_app: appSchema.optional().nullable(), - pull_request: z - .object({ - url: z.string().optional(), - html_url: z.string().optional(), - diff_url: z.string().optional(), - patch_url: z.string().optional(), - merged_at: z.string().optional().nullable(), - }) - .optional(), - body: z.string().nullable(), - reactions: reactionsSchema, - timeline_url: z.string().optional(), - state_reason: z.string().optional().nullable(), -}); - -export const issuesAssignedEventSchema = z.object({ - action: z.literal("assigned"), - issue: issueSchema, - assignee: userSchema.optional().nullable(), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesClosedEventSchema = z.object({ - action: z.literal("closed"), - issue: issueSchema.and( - z.object({ - state: z.literal("closed"), - closed_at: z.string(), - }) - ), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesDeletedEventSchema = z.object({ - action: z.literal("deleted"), - issue: issueSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesDemilestonedEventSchema = z.object({ - action: z.literal("demilestoned"), - issue: issueSchema.and( - z.object({ - milestone: z.null(), - }) - ), - milestone: milestoneSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesEditedEventSchema = z.object({ - action: z.literal("edited"), - issue: issueSchema, - label: labelSchema.optional(), - changes: z.object({ - body: z - .object({ - from: z.string(), - }) - .optional(), - title: z - .object({ - from: z.string(), - }) - .optional(), - }), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesLabeledEventSchema = z.object({ - action: z.literal("labeled"), - issue: issueSchema, - label: labelSchema.optional(), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesLockedEventSchema = z.object({ - action: z.literal("locked"), - issue: issueSchema.and( - z.object({ - locked: z.literal(true), - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), - }) - ), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesMilestonedEventSchema = z.object({ - action: z.literal("milestoned"), - issue: issueSchema.and( - z.object({ - milestone: milestoneSchema, - }) - ), - milestone: milestoneSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesOpenedEventSchema = z.object({ - action: z.literal("opened"), - changes: z - .object({ - old_issue: issueSchema, - old_repository: repositorySchema, - }) - .optional(), - issue: issueSchema.and( - z.object({ - state: z.literal("open"), - closed_at: z.null(), - }) - ), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesPinnedEventSchema = z.object({ - action: z.literal("pinned"), - issue: issueSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesReopenedEventSchema = z.object({ - action: z.literal("reopened"), - issue: issueSchema.and( - z.object({ - state: z.literal("open"), - }) - ), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesTransferredEventSchema = z.object({ - action: z.literal("transferred"), - changes: z.object({ - new_issue: issueSchema, - new_repository: repositorySchema, - }), - issue: issueSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesUnassignedEventSchema = z.object({ - action: z.literal("unassigned"), - issue: issueSchema, - assignee: userSchema.optional().nullable(), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesUnlabeledEventSchema = z.object({ - action: z.literal("unlabeled"), - issue: issueSchema, - label: labelSchema.optional(), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesUnlockedEventSchema = z.object({ - action: z.literal("unlocked"), - issue: issueSchema.and( - z.object({ - locked: z.literal(false), - active_lock_reason: z.null(), - }) - ), - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesUnpinnedEventSchema = z.object({ - action: z.literal("unpinned"), - issue: issueSchema, - repository: repositorySchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); - -export const issuesEventSchema: z.ZodDiscriminatedUnion< - "action", - [ - typeof issuesAssignedEventSchema, - typeof issuesClosedEventSchema, - typeof issuesDeletedEventSchema, - typeof issuesDemilestonedEventSchema, - typeof issuesEditedEventSchema, - typeof issuesLabeledEventSchema, - typeof issuesLockedEventSchema, - typeof issuesMilestonedEventSchema, - typeof issuesOpenedEventSchema, - typeof issuesPinnedEventSchema, - typeof issuesReopenedEventSchema, - typeof issuesTransferredEventSchema, - typeof issuesUnassignedEventSchema, - typeof issuesUnlabeledEventSchema, - typeof issuesUnlockedEventSchema, - typeof issuesUnpinnedEventSchema - ] -> = z.discriminatedUnion("action", [ - issuesAssignedEventSchema, - issuesClosedEventSchema, - issuesDeletedEventSchema, - issuesDemilestonedEventSchema, - issuesEditedEventSchema, - issuesLabeledEventSchema, - issuesLockedEventSchema, - issuesMilestonedEventSchema, - issuesOpenedEventSchema, - issuesPinnedEventSchema, - issuesReopenedEventSchema, - issuesTransferredEventSchema, - issuesUnassignedEventSchema, - issuesUnlabeledEventSchema, - issuesUnlockedEventSchema, - issuesUnpinnedEventSchema, -]); diff --git a/packages/trigger-providers/src/providers/github/schemas/pullRequestComments.ts b/packages/trigger-providers/src/providers/github/schemas/pullRequestComments.ts deleted file mode 100644 index f386dd2f2b4..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/pullRequestComments.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { z } from "zod"; -import { - userSchema, - teamSchema, - labelSchema, - milestoneSchema, - repositorySchema, - linkSchema, - authorAssociationSchema, - installationLiteSchema, - organizationSchema, - reactionsSchema, - pullRequestAutoMergeSchema, -} from "./shared"; - -export const pullRequestReviewCommentSchema = z.object({ - url: z.string(), - pull_request_review_id: z.number(), - id: z.number(), - node_id: z.string(), - diff_hunk: z.string(), - path: z.string(), - position: z.number().nullable(), - original_position: z.number(), - commit_id: z.string(), - original_commit_id: z.string(), - user: userSchema, - body: z.string(), - created_at: z.string(), - updated_at: z.string(), - html_url: z.string(), - pull_request_url: z.string(), - author_association: authorAssociationSchema, - _links: z.object({ - self: linkSchema, - html: linkSchema, - pull_request: linkSchema, - }), - reactions: reactionsSchema, - start_line: z.number().nullable(), - original_start_line: z.number().nullable(), - start_side: z.union([z.literal("LEFT"), z.literal("RIGHT")]).nullable(), - line: z.number().nullable(), - original_line: z.number(), - side: z.union([z.literal("LEFT"), z.literal("RIGHT")]), - in_reply_to_id: z.number().optional(), -}); - -export const pullRequestReviewCommentCreatedEventSchema = z.object({ - action: z.literal("created"), - comment: pullRequestReviewCommentSchema, - pull_request: z.object({ - url: z.string(), - id: z.number(), - node_id: z.string(), - html_url: z.string(), - diff_url: z.string(), - patch_url: z.string(), - issue_url: z.string(), - number: z.number(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - title: z.string(), - user: userSchema, - body: z.string().nullable(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - merged_at: z.string().nullable(), - merge_commit_sha: z.string().nullable(), - assignee: userSchema.nullable(), - assignees: z.array(userSchema), - requested_reviewers: z.array(z.union([userSchema, teamSchema])), - requested_teams: z.array(teamSchema), - labels: z.array(labelSchema), - milestone: milestoneSchema.nullable(), - draft: z.boolean().optional(), - commits_url: z.string(), - review_comments_url: z.string(), - review_comment_url: z.string(), - comments_url: z.string(), - statuses_url: z.string(), - head: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - base: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - _links: z.object({ - self: linkSchema, - html: linkSchema, - issue: linkSchema, - comments: linkSchema, - review_comments: linkSchema, - review_comment: linkSchema, - commits: linkSchema, - statuses: linkSchema, - }), - auto_merge: pullRequestAutoMergeSchema.optional().nullable(), - author_association: authorAssociationSchema, - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), - }), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewCommentDeletedEventSchema = z.object({ - action: z.literal("deleted"), - comment: pullRequestReviewCommentSchema, - pull_request: z.object({ - url: z.string(), - id: z.number(), - node_id: z.string(), - html_url: z.string(), - diff_url: z.string(), - patch_url: z.string(), - issue_url: z.string(), - number: z.number(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - title: z.string(), - user: userSchema, - body: z.string().nullable(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - merged_at: z.string().nullable(), - merge_commit_sha: z.string().nullable(), - assignee: userSchema.nullable(), - assignees: z.array(userSchema), - requested_reviewers: z.array(z.union([userSchema, teamSchema])), - requested_teams: z.array(teamSchema), - labels: z.array(labelSchema), - milestone: milestoneSchema.nullable(), - draft: z.boolean().optional(), - commits_url: z.string(), - review_comments_url: z.string(), - review_comment_url: z.string(), - comments_url: z.string(), - statuses_url: z.string(), - head: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - base: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - _links: z.object({ - self: linkSchema, - html: linkSchema, - issue: linkSchema, - comments: linkSchema, - review_comments: linkSchema, - review_comment: linkSchema, - commits: linkSchema, - statuses: linkSchema, - }), - auto_merge: pullRequestAutoMergeSchema.optional().nullable(), - author_association: authorAssociationSchema, - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), - }), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewCommentEditedEventSchema = z.object({ - action: z.literal("edited"), - changes: z.object({ - body: z - .object({ - from: z.string(), - }) - .optional(), - }), - comment: pullRequestReviewCommentSchema, - pull_request: z.object({ - url: z.string(), - id: z.number(), - node_id: z.string(), - html_url: z.string(), - diff_url: z.string(), - patch_url: z.string(), - issue_url: z.string(), - number: z.number(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - title: z.string(), - user: userSchema, - body: z.string().nullable(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - merged_at: z.string().nullable(), - merge_commit_sha: z.string().nullable(), - assignee: userSchema.nullable(), - assignees: z.array(userSchema), - requested_reviewers: z.array(z.union([userSchema, teamSchema])), - requested_teams: z.array(teamSchema), - labels: z.array(labelSchema), - milestone: milestoneSchema.nullable(), - draft: z.boolean().optional(), - commits_url: z.string(), - review_comments_url: z.string(), - review_comment_url: z.string(), - comments_url: z.string(), - statuses_url: z.string(), - head: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - base: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - _links: z.object({ - self: linkSchema, - html: linkSchema, - issue: linkSchema, - comments: linkSchema, - review_comments: linkSchema, - review_comment: linkSchema, - commits: linkSchema, - statuses: linkSchema, - }), - auto_merge: pullRequestAutoMergeSchema.optional().nullable(), - author_association: authorAssociationSchema, - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), - }), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewCommentEventSchema = z.union([ - pullRequestReviewCommentCreatedEventSchema, - pullRequestReviewCommentDeletedEventSchema, - pullRequestReviewCommentEditedEventSchema, -]); diff --git a/packages/trigger-providers/src/providers/github/schemas/pullRequestReviews.ts b/packages/trigger-providers/src/providers/github/schemas/pullRequestReviews.ts deleted file mode 100644 index 378ade5b438..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/pullRequestReviews.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { z } from "zod"; -import { - repositorySchema, - installationLiteSchema, - organizationSchema, - userSchema, - authorAssociationSchema, - linkSchema, - simplePullRequestSchema, -} from "./shared"; - -export const pullRequestReviewSchema = z.object({ - id: z.number(), - node_id: z.string(), - user: userSchema, - body: z.string().nullable(), - commit_id: z.string(), - submitted_at: z.string().nullable(), - state: z.union([ - z.literal("commented"), - z.literal("changes_requested"), - z.literal("approved"), - z.literal("dismissed"), - ]), - html_url: z.string(), - pull_request_url: z.string(), - author_association: authorAssociationSchema, - _links: z.object({ - html: linkSchema, - pull_request: linkSchema, - }), -}); - -export const pullRequestReviewDismissedEventSchema = z.object({ - action: z.literal("dismissed"), - review: pullRequestReviewSchema.and( - z.object({ - state: z.literal("dismissed"), - }) - ), - pull_request: simplePullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewEditedEventSchema = z.object({ - action: z.literal("edited"), - changes: z.object({ - body: z - .object({ - from: z.string(), - }) - .optional(), - }), - review: pullRequestReviewSchema, - pull_request: simplePullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewSubmittedEventSchema = z.object({ - action: z.literal("submitted"), - review: pullRequestReviewSchema, - pull_request: simplePullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewEventSchema = z.union([ - pullRequestReviewDismissedEventSchema, - pullRequestReviewEditedEventSchema, - pullRequestReviewSubmittedEventSchema, -]); diff --git a/packages/trigger-providers/src/providers/github/schemas/pullRequests.ts b/packages/trigger-providers/src/providers/github/schemas/pullRequests.ts deleted file mode 100644 index 448bd754b5c..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/pullRequests.ts +++ /dev/null @@ -1,401 +0,0 @@ -import { z } from "zod"; -import { - authorAssociationSchema, - installationLiteSchema, - labelSchema, - linkSchema, - milestoneSchema, - organizationSchema, - pullRequestAutoMergeSchema, - repositorySchema, - teamSchema, - userSchema, -} from "./shared"; - -export const pullRequestSchema = z.object({ - url: z.string(), - id: z.number(), - node_id: z.string(), - html_url: z.string(), - diff_url: z.string(), - patch_url: z.string(), - issue_url: z.string(), - number: z.number(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - title: z.string(), - user: userSchema, - body: z.string().nullable(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - merged_at: z.string().nullable(), - merge_commit_sha: z.string().nullable(), - assignee: userSchema.nullable(), - assignees: z.array(userSchema), - requested_reviewers: z.array(z.union([userSchema, teamSchema])), - requested_teams: z.array(teamSchema), - labels: z.array(labelSchema), - milestone: milestoneSchema.nullable(), - commits_url: z.string(), - review_comments_url: z.string(), - review_comment_url: z.string(), - comments_url: z.string(), - statuses_url: z.string(), - head: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - base: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - _links: z.object({ - self: linkSchema, - html: linkSchema, - issue: linkSchema, - comments: linkSchema, - review_comments: linkSchema, - review_comment: linkSchema, - commits: linkSchema, - statuses: linkSchema, - }), - author_association: authorAssociationSchema, - auto_merge: pullRequestAutoMergeSchema.nullable(), - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), - draft: z.boolean(), - merged: z.boolean().nullable(), - mergeable: z.boolean().nullable(), - rebaseable: z.boolean().nullable(), - mergeable_state: z.string(), - merged_by: userSchema.nullable(), - comments: z.number(), - review_comments: z.number(), - maintainer_can_modify: z.boolean(), - commits: z.number(), - additions: z.number(), - deletions: z.number(), - changed_files: z.number(), -}); - -export const pullRequestAssignedEventSchema = z.object({ - action: z.literal("assigned"), - number: z.number(), - pull_request: pullRequestSchema, - assignee: userSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestAutoMergeDisabledEventSchema = z.object({ - action: z.literal("auto_merge_disabled"), - number: z.number(), - pull_request: pullRequestSchema, - reason: z.string(), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestAutoMergeEnabledEventSchema = z.object({ - action: z.literal("auto_merge_enabled"), - number: z.number(), - pull_request: pullRequestSchema, - reason: z.string(), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestClosedEventSchema = z.object({ - action: z.literal("closed"), - number: z.number(), - pull_request: pullRequestSchema.and( - z.object({ - state: z.literal("closed"), - closed_at: z.string(), - merged: z.boolean(), - }) - ), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestConvertedToDraftEventSchema = z.object({ - action: z.literal("converted_to_draft"), - number: z.number(), - pull_request: pullRequestSchema.and( - z.object({ - closed_at: z.null(), - merged_at: z.null(), - draft: z.literal(true), - merged: z.literal(false), - merged_by: z.null(), - }) - ), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestDequeuedEventSchema = z.object({ - action: z.literal("dequeued"), - number: z.number(), - reason: z.string(), - pull_request: pullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestEditedEventSchema = z.object({ - action: z.literal("edited"), - number: z.number(), - changes: z.object({ - body: z - .object({ - from: z.string(), - }) - .optional(), - title: z - .object({ - from: z.string(), - }) - .optional(), - base: z - .object({ - ref: z.object({ - from: z.string(), - }), - sha: z.object({ - from: z.string(), - }), - }) - .optional(), - }), - pull_request: pullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestLabeledEventSchema = z.object({ - action: z.literal("labeled"), - number: z.number(), - pull_request: pullRequestSchema, - label: labelSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestLockedEventSchema = z.object({ - action: z.literal("locked"), - number: z.number(), - pull_request: pullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestOpenedEventSchema = z.object({ - action: z.literal("opened"), - number: z.number(), - pull_request: pullRequestSchema.and( - z.object({ - state: z.literal("open"), - closed_at: z.null(), - merged_at: z.null(), - active_lock_reason: z.null(), - merged_by: z.null(), - }) - ), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestQueuedEventSchema = z.object({ - action: z.literal("queued"), - number: z.number(), - pull_request: pullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReadyForReviewEventSchema = z.object({ - action: z.literal("ready_for_review"), - number: z.number(), - pull_request: pullRequestSchema.and( - z.object({ - state: z.literal("open"), - closed_at: z.null(), - merged_at: z.null(), - draft: z.literal(false), - merged: z.boolean(), - merged_by: z.null(), - }) - ), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReopenedEventSchema = z.object({ - action: z.literal("reopened"), - number: z.number(), - pull_request: pullRequestSchema.and( - z.object({ - state: z.literal("open"), - closed_at: z.null(), - merged_at: z.null(), - merged: z.boolean(), - merged_by: z.null(), - }) - ), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestSynchronizeEventSchema = z.object({ - action: z.literal("synchronize"), - number: z.number(), - before: z.string(), - after: z.string(), - pull_request: pullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestUnassignedEventSchema = z.object({ - action: z.literal("unassigned"), - number: z.number(), - pull_request: pullRequestSchema, - assignee: userSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestUnlabeledEventSchema = z.object({ - action: z.literal("unlabeled"), - number: z.number(), - pull_request: pullRequestSchema, - label: labelSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestUnlockedEventSchema = z.object({ - action: z.literal("unlocked"), - number: z.number(), - pull_request: pullRequestSchema, - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewRequestRemovedEventSchema = z.object({ - action: z.literal("review_request_removed"), - number: z.number(), - pull_request: pullRequestSchema, - requested_reviewer: userSchema.optional(), - requested_team: teamSchema.optional(), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestReviewRequestedEventSchema = z.object({ - action: z.literal("review_requested"), - number: z.number(), - pull_request: pullRequestSchema, - requested_reviewer: userSchema.optional(), - requested_team: teamSchema.optional(), - repository: repositorySchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), - sender: userSchema, -}); - -export const pullRequestEventSchema: z.ZodDiscriminatedUnion< - "action", - [ - typeof pullRequestAssignedEventSchema, - typeof pullRequestAutoMergeDisabledEventSchema, - typeof pullRequestAutoMergeEnabledEventSchema, - typeof pullRequestClosedEventSchema, - typeof pullRequestConvertedToDraftEventSchema, - typeof pullRequestDequeuedEventSchema, - typeof pullRequestEditedEventSchema, - typeof pullRequestLabeledEventSchema, - typeof pullRequestLockedEventSchema, - typeof pullRequestOpenedEventSchema, - typeof pullRequestQueuedEventSchema, - typeof pullRequestReadyForReviewEventSchema, - typeof pullRequestReopenedEventSchema, - typeof pullRequestReviewRequestRemovedEventSchema, - typeof pullRequestReviewRequestedEventSchema, - typeof pullRequestSynchronizeEventSchema, - typeof pullRequestUnassignedEventSchema, - typeof pullRequestUnlabeledEventSchema, - typeof pullRequestUnlockedEventSchema - ] -> = z.discriminatedUnion("action", [ - pullRequestAssignedEventSchema, - pullRequestAutoMergeDisabledEventSchema, - pullRequestAutoMergeEnabledEventSchema, - pullRequestClosedEventSchema, - pullRequestConvertedToDraftEventSchema, - pullRequestDequeuedEventSchema, - pullRequestEditedEventSchema, - pullRequestLabeledEventSchema, - pullRequestLockedEventSchema, - pullRequestOpenedEventSchema, - pullRequestQueuedEventSchema, - pullRequestReadyForReviewEventSchema, - pullRequestReopenedEventSchema, - pullRequestReviewRequestRemovedEventSchema, - pullRequestReviewRequestedEventSchema, - pullRequestSynchronizeEventSchema, - pullRequestUnassignedEventSchema, - pullRequestUnlabeledEventSchema, - pullRequestUnlockedEventSchema, -]); diff --git a/packages/trigger-providers/src/providers/github/schemas/pushes.ts b/packages/trigger-providers/src/providers/github/schemas/pushes.ts deleted file mode 100644 index d1996500430..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/pushes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from "zod"; -import { - repositorySchema, - userSchema, - installationLiteSchema, - organizationSchema, - commitSchema, - committerSchema, -} from "./shared"; - -export const pushEventSchema = z.object({ - ref: z.string(), - before: z.string(), - after: z.string(), - created: z.boolean(), - deleted: z.boolean(), - forced: z.boolean(), - base_ref: z.string().nullable(), - compare: z.string(), - commits: z.array(commitSchema), - head_commit: commitSchema.nullable(), - repository: repositorySchema, - pusher: committerSchema, - sender: userSchema, - installation: installationLiteSchema.optional(), - organization: organizationSchema.optional(), -}); diff --git a/packages/trigger-providers/src/providers/github/schemas/shared.ts b/packages/trigger-providers/src/providers/github/schemas/shared.ts deleted file mode 100644 index d57901baa7b..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/shared.ts +++ /dev/null @@ -1,507 +0,0 @@ -import { z } from "zod"; - -export const userSchema = z.object({ - login: z.string(), - id: z.number(), - node_id: z.string(), - name: z.string().optional(), - email: z.string().optional().nullable(), - avatar_url: z.string(), - gravatar_id: z.string(), - url: z.string(), - html_url: z.string(), - followers_url: z.string(), - following_url: z.string(), - gists_url: z.string(), - starred_url: z.string(), - subscriptions_url: z.string(), - organizations_url: z.string(), - repos_url: z.string(), - events_url: z.string(), - received_events_url: z.string(), - type: z.union([ - z.literal("Bot"), - z.literal("User"), - z.literal("Organization"), - ]), - site_admin: z.boolean(), -}); - -export const licenseSchema = z.object({ - key: z.string(), - name: z.string(), - spdx_id: z.string(), - url: z.string().nullable(), - node_id: z.string(), -}); - -export const installationLiteSchema = z.object({ - id: z.number(), - node_id: z.string(), -}); - -export const organizationSchema = z.object({ - login: z.string(), - id: z.number(), - node_id: z.string(), - url: z.string(), - html_url: z.string().optional(), - repos_url: z.string(), - events_url: z.string(), - hooks_url: z.string(), - issues_url: z.string(), - members_url: z.string(), - public_members_url: z.string(), - avatar_url: z.string(), - description: z.string().nullable(), -}); - -export const repositorySchema = z.object({ - id: z.number(), - node_id: z.string(), - name: z.string(), - full_name: z.string(), - private: z.boolean(), - owner: userSchema, - html_url: z.string(), - description: z.string().nullable(), - fork: z.boolean(), - url: z.string(), - forks_url: z.string(), - keys_url: z.string(), - collaborators_url: z.string(), - teams_url: z.string(), - hooks_url: z.string(), - issue_events_url: z.string(), - events_url: z.string(), - assignees_url: z.string(), - branches_url: z.string(), - tags_url: z.string(), - blobs_url: z.string(), - git_tags_url: z.string(), - git_refs_url: z.string(), - trees_url: z.string(), - statuses_url: z.string(), - languages_url: z.string(), - stargazers_url: z.string(), - contributors_url: z.string(), - subscribers_url: z.string(), - subscription_url: z.string(), - commits_url: z.string(), - git_commits_url: z.string(), - comments_url: z.string(), - issue_comment_url: z.string(), - contents_url: z.string(), - compare_url: z.string(), - merges_url: z.string(), - archive_url: z.string(), - downloads_url: z.string(), - issues_url: z.string(), - pulls_url: z.string(), - milestones_url: z.string(), - notifications_url: z.string(), - labels_url: z.string(), - releases_url: z.string(), - deployments_url: z.string(), - created_at: z.union([z.number(), z.string()]), - updated_at: z.string(), - pushed_at: z.union([z.number(), z.string()]).nullable(), - git_url: z.string(), - ssh_url: z.string(), - clone_url: z.string(), - svn_url: z.string(), - homepage: z.string().nullable(), - size: z.number(), - stargazers_count: z.number(), - watchers_count: z.number(), - language: z.string().nullable(), - has_issues: z.boolean(), - has_projects: z.boolean(), - has_downloads: z.boolean(), - has_wiki: z.boolean(), - has_pages: z.boolean(), - forks_count: z.number(), - mirror_url: z.string().nullable(), - archived: z.boolean(), - disabled: z.boolean().optional(), - open_issues_count: z.number(), - license: licenseSchema.nullable(), - forks: z.number(), - open_issues: z.number(), - watchers: z.number(), - stargazers: z.number().optional(), - default_branch: z.string(), - allow_squash_merge: z.boolean().optional(), - allow_merge_commit: z.boolean().optional(), - allow_rebase_merge: z.boolean().optional(), - allow_auto_merge: z.boolean().optional(), - allow_forking: z.boolean().optional(), - allow_update_branch: z.boolean().optional(), - use_squash_pr_title_as_default: z.boolean().optional(), - squash_merge_commit_message: z.string().optional(), - squash_merge_commit_title: z.string().optional(), - merge_commit_message: z.string().optional(), - merge_commit_title: z.string().optional(), - is_template: z.boolean(), - web_commit_signoff_required: z.boolean(), - topics: z.array(z.string()), - visibility: z.union([ - z.literal("public"), - z.literal("private"), - z.literal("internal"), - ]), - delete_branch_on_merge: z.boolean().optional(), - master_branch: z.string().optional(), - permissions: z - .object({ - pull: z.boolean(), - push: z.boolean(), - admin: z.boolean(), - maintain: z.boolean().optional(), - triage: z.boolean().optional(), - }) - .optional(), - public: z.boolean().optional(), - organization: z.string().optional(), -}); - -export const teamSchema = z.object({ - name: z.string(), - id: z.number(), - node_id: z.string(), - slug: z.string(), - description: z.string().nullable(), - privacy: z.union([ - z.literal("open"), - z.literal("closed"), - z.literal("secret"), - ]), - url: z.string(), - html_url: z.string(), - members_url: z.string(), - repositories_url: z.string(), - permission: z.string(), - parent: z - .object({ - name: z.string(), - id: z.number(), - node_id: z.string(), - slug: z.string(), - description: z.string().nullable(), - privacy: z.union([ - z.literal("open"), - z.literal("closed"), - z.literal("secret"), - ]), - url: z.string(), - html_url: z.string(), - members_url: z.string(), - repositories_url: z.string(), - permission: z.string(), - }) - .optional() - .nullable(), -}); - -export const linkSchema = z.object({ - href: z.string(), -}); - -export const appSchema = z.object({ - id: z.number(), - slug: z.string().optional(), - node_id: z.string(), - owner: userSchema, - name: z.string(), - description: z.string().nullable(), - external_url: z.string(), - html_url: z.string(), - created_at: z.string(), - updated_at: z.string(), - permissions: z - .object({ - actions: z.union([z.literal("read"), z.literal("write")]).optional(), - administration: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - blocking: z.union([z.literal("read"), z.literal("write")]).optional(), - checks: z.union([z.literal("read"), z.literal("write")]).optional(), - content_references: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - contents: z.union([z.literal("read"), z.literal("write")]).optional(), - deployments: z.union([z.literal("read"), z.literal("write")]).optional(), - discussions: z.union([z.literal("read"), z.literal("write")]).optional(), - emails: z.union([z.literal("read"), z.literal("write")]).optional(), - environments: z.union([z.literal("read"), z.literal("write")]).optional(), - issues: z.union([z.literal("read"), z.literal("write")]).optional(), - keys: z.union([z.literal("read"), z.literal("write")]).optional(), - members: z.union([z.literal("read"), z.literal("write")]).optional(), - merge_queues: z.union([z.literal("read"), z.literal("write")]).optional(), - metadata: z.union([z.literal("read"), z.literal("write")]).optional(), - organization_administration: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_hooks: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_packages: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_plan: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_projects: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_secrets: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_self_hosted_runners: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - organization_user_blocking: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - packages: z.union([z.literal("read"), z.literal("write")]).optional(), - pages: z.union([z.literal("read"), z.literal("write")]).optional(), - pull_requests: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - repository_hooks: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - repository_projects: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - secret_scanning_alerts: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - secrets: z.union([z.literal("read"), z.literal("write")]).optional(), - security_events: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - security_scanning_alert: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - single_file: z.union([z.literal("read"), z.literal("write")]).optional(), - statuses: z.union([z.literal("read"), z.literal("write")]).optional(), - team_discussions: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - vulnerability_alerts: z - .union([z.literal("read"), z.literal("write")]) - .optional(), - workflows: z.union([z.literal("read"), z.literal("write")]).optional(), - }) - .optional(), - events: z - .array( - z.union([ - z.literal("branch_protection_rule"), - z.literal("check_run"), - z.literal("check_suite"), - z.literal("code_scanning_alert"), - z.literal("commit_comment"), - z.literal("content_reference"), - z.literal("create"), - z.literal("delete"), - z.literal("deployment"), - z.literal("deployment_review"), - z.literal("deployment_status"), - z.literal("deploy_key"), - z.literal("discussion"), - z.literal("discussion_comment"), - z.literal("fork"), - z.literal("gollum"), - z.literal("issues"), - z.literal("issue_comment"), - z.literal("label"), - z.literal("member"), - z.literal("membership"), - z.literal("merge_group"), - z.literal("merge_queue_entry"), - z.literal("milestone"), - z.literal("organization"), - z.literal("org_block"), - z.literal("page_build"), - z.literal("project"), - z.literal("projects_v2_item"), - z.literal("project_card"), - z.literal("project_column"), - z.literal("public"), - z.literal("pull_request"), - z.literal("pull_request_review"), - z.literal("pull_request_review_comment"), - z.literal("push"), - z.literal("registry_package"), - z.literal("release"), - z.literal("repository"), - z.literal("repository_dispatch"), - z.literal("secret_scanning_alert"), - z.literal("secret_scanning_alert_location"), - z.literal("security_and_analysis"), - z.literal("star"), - z.literal("status"), - z.literal("team"), - z.literal("team_add"), - z.literal("watch"), - z.literal("workflow_dispatch"), - z.literal("workflow_run"), - z.literal("workflow_job"), - ]) - ) - .optional(), -}); - -export const reactionsSchema = z.object({ - url: z.string(), - total_count: z.number(), - "+1": z.number(), - "-1": z.number(), - laugh: z.number(), - hooray: z.number(), - confused: z.number(), - heart: z.number(), - rocket: z.number(), - eyes: z.number(), -}); - -export const authorAssociationSchema = z.union([ - z.literal("COLLABORATOR"), - z.literal("CONTRIBUTOR"), - z.literal("FIRST_TIMER"), - z.literal("FIRST_TIME_CONTRIBUTOR"), - z.literal("MANNEQUIN"), - z.literal("MEMBER"), - z.literal("NONE"), - z.literal("OWNER"), -]); - -export const milestoneSchema = z.object({ - url: z.string(), - html_url: z.string(), - labels_url: z.string(), - id: z.number(), - node_id: z.string(), - number: z.number(), - title: z.string(), - description: z.string().nullable(), - creator: userSchema, - open_issues: z.number(), - closed_issues: z.number(), - state: z.union([z.literal("open"), z.literal("closed")]), - created_at: z.string(), - updated_at: z.string(), - due_on: z.string().nullable(), - closed_at: z.string().nullable(), -}); - -export const labelSchema = z.object({ - id: z.number(), - node_id: z.string(), - url: z.string(), - name: z.string(), - description: z.string().nullable(), - color: z.string(), - default: z.boolean(), -}); - -export const pullRequestAutoMergeSchema = z.object({ - enabled_by: userSchema, - merge_method: z.union([ - z.literal("merge"), - z.literal("squash"), - z.literal("rebase"), - ]), - commit_title: z.string(), - commit_message: z.string(), -}); - -export const simplePullRequestSchema = z.object({ - url: z.string(), - id: z.number(), - node_id: z.string(), - html_url: z.string(), - diff_url: z.string(), - patch_url: z.string(), - issue_url: z.string(), - number: z.number(), - state: z.union([z.literal("open"), z.literal("closed")]), - locked: z.boolean(), - title: z.string(), - user: userSchema, - body: z.string().nullable(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - merged_at: z.string().nullable(), - merge_commit_sha: z.string().nullable(), - assignee: userSchema.nullable(), - assignees: z.array(userSchema), - requested_reviewers: z.array(z.union([userSchema, teamSchema])), - requested_teams: z.array(teamSchema), - labels: z.array(labelSchema), - milestone: milestoneSchema.nullable(), - draft: z.boolean(), - commits_url: z.string(), - review_comments_url: z.string(), - review_comment_url: z.string(), - comments_url: z.string(), - statuses_url: z.string(), - head: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - base: z.object({ - label: z.string(), - ref: z.string(), - sha: z.string(), - user: userSchema, - repo: repositorySchema, - }), - _links: z.object({ - self: linkSchema, - html: linkSchema, - issue: linkSchema, - comments: linkSchema, - review_comments: linkSchema, - review_comment: linkSchema, - commits: linkSchema, - statuses: linkSchema, - }), - author_association: authorAssociationSchema, - auto_merge: pullRequestAutoMergeSchema.nullable(), - active_lock_reason: z - .union([ - z.literal("resolved"), - z.literal("off-topic"), - z.literal("too heated"), - z.literal("spam"), - ]) - .nullable(), -}); - -export const committerSchema = z.object({ - name: z.string(), - email: z.string().nullable(), - date: z.string().optional(), - username: z.string().optional(), -}); - -export const commitSchema = z.object({ - id: z.string(), - tree_id: z.string(), - distinct: z.boolean(), - message: z.string(), - timestamp: z.string(), - url: z.string(), - author: committerSchema, - committer: committerSchema, - added: z.array(z.string()), - modified: z.array(z.string()), - removed: z.array(z.string()), -}); diff --git a/packages/trigger-providers/src/providers/github/schemas/stars.ts b/packages/trigger-providers/src/providers/github/schemas/stars.ts deleted file mode 100644 index 490809391db..00000000000 --- a/packages/trigger-providers/src/providers/github/schemas/stars.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod"; -import { organizationSchema, repositorySchema, userSchema } from "./shared"; - -export const starCreatedEventSchema = z.object({ - action: z.literal("created"), - starred_at: z.coerce.date(), - repository: repositorySchema, - sender: userSchema, - organization: organizationSchema.optional(), -}); - -export const starDeletedEventSchema = z.object({ - action: z.literal("deleted"), - repository: repositorySchema, - sender: userSchema, - organization: organizationSchema.optional(), -}); - -export const starEventSchema = z.discriminatedUnion("action", [ - starCreatedEventSchema, - starDeletedEventSchema, -]); diff --git a/packages/trigger-providers/src/providers/resend/index.ts b/packages/trigger-providers/src/providers/resend/index.ts deleted file mode 100644 index 0062c01b2d1..00000000000 --- a/packages/trigger-providers/src/providers/resend/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as schemas from "./schemas"; - -export const resend = { - name: "Resend", - slug: "resend", - icon: "/integrations/resend.png", - enabledFor: "all", - authentication: { - type: "api_key", - header_name: "Authorization", - header_type: "access_token", - documentation: `1. Login to [Resend](https://resend.com) -2. Go to the API Keys page -3. Generate a new API key -4. Paste it into the field below`, - }, - schemas, -}; diff --git a/packages/trigger-providers/src/providers/resend/schemas.ts b/packages/trigger-providers/src/providers/resend/schemas.ts deleted file mode 100644 index 3eb6dd9a5d2..00000000000 --- a/packages/trigger-providers/src/providers/resend/schemas.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ReactElement } from "react"; -import { z } from "zod"; - -const ReactElementSchema = z.custom(); - -export const BaseSendFieldsSchema = z.object({ - from: z.string(), - to: z.union([z.string(), z.array(z.string())]), - bcc: z.union([z.string(), z.array(z.string())]).optional(), - cc: z.union([z.string(), z.array(z.string())]).optional(), - replyTo: z.union([z.string(), z.array(z.string())]).optional(), - subject: z.string().optional(), -}); - -export const SendEmailOptionsSchema = z - .object({ - text: z.string().optional(), - html: z.string().optional(), - react: ReactElementSchema.optional(), - }) - .and(BaseSendFieldsSchema); - -export const SendEmailBodySchema = BaseSendFieldsSchema.omit({ - replyTo: true, -}).and( - z.object({ - reply_to: z.string().optional(), - text: z.string().optional(), - html: z.string().optional(), - }) -); - -export const SendEmailSuccessResponseSchema = z.object({ - id: z.string(), - from: z.string(), - to: z.string(), - created_at: z.string().optional(), -}); -export const SendEmailErrorResponseSchema = z.object({ - error: z.object({ code: z.number(), type: z.string(), message: z.string() }), -}); -export const SendEmailResponseSchema = z.union([ - SendEmailSuccessResponseSchema, - SendEmailErrorResponseSchema, -]); diff --git a/packages/trigger-providers/src/providers/shopify/index.ts b/packages/trigger-providers/src/providers/shopify/index.ts deleted file mode 100644 index 56cb204180e..00000000000 --- a/packages/trigger-providers/src/providers/shopify/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as schemas from "./schemas"; - -export const shopify = { - name: "Shopify", - slug: "shopify", - icon: "/integrations/shopify.png", - enabledFor: "all", - authentication: { - type: "api_key", - header_name: "X-Shopify-Access-Token", - header_type: "access_token", - additionalFields: [ - { - key: "store_name", - fieldType: "text", - name: "Store name", - placeholder: "mystore", - description: `This is the name of your Shopify store`, - }, - ], - documentation: `1. Follow [this guide](https://help.shopify.com/en/manual/apps/custom-apps) to enable Custom apps`, - }, - schemas, -}; diff --git a/packages/trigger-providers/src/providers/shopify/schemas.ts b/packages/trigger-providers/src/providers/shopify/schemas.ts deleted file mode 100644 index bfa377a44ca..00000000000 --- a/packages/trigger-providers/src/providers/shopify/schemas.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { z } from "zod"; -import { parseGid } from "@shopify/admin-graphql-api-utilities"; - -export const FirstOrLastSchema = z - .object({ - first: z.number().optional(), - }) - .default({ first: 100 }); - -const objectWithId = z.object({ - id: z.string(), -}); - -const ProductStatusSchema = z.union([ - z.literal("ACTIVE"), - z.literal("ARCHIVED"), - z.literal("DRAFT"), -]); - -export const ProductSchema = z.object({ - id: z.string(), - title: z.string(), - status: ProductStatusSchema, - createdAt: z.string().nullable(), - updatedAt: z.string().nullable(), - description: z.string().nullable(), - descriptionHtml: z.string().nullable(), - featuredImage: objectWithId.nullable(), - handle: z.string().nullable(), - hasOnlyDefaultVariant: z.boolean().nullable(), - hasOutOfStockVariants: z.boolean().nullable(), - options: z.array(z.object({ name: z.string() })).nullable(), - onlineStorePreviewUrl: z.string().nullable(), - onlineStoreUrl: z.string().nullable(), - priceRange: z - .object({ - maxVariantPrice: z.object({ amount: z.string() }), - minVariantPrice: z.object({ amount: z.string() }), - }) - .nullable(), - productType: z.string().nullable(), - tags: z.array(z.string()).nullable(), - totalInventory: z.number().nullable(), - totalVariants: z.number().nullable(), - tracksInventory: z.boolean().nullable(), - variants: z.array(objectWithId).nullable(), -}); - -export const ProductVariantSchema = z.object({ - id: z.string(), - title: z.string().nullable(), - createdAt: z.string().nullable(), - updatedAt: z.string().nullable(), - price: z.string().nullable(), - product: z.object({ - id: z.string(), - title: z.string(), - }), - sku: z.string().nullable(), - barcode: z.string().nullable(), - compareAtPrice: z.string().nullable(), - fulfillmentService: objectWithId.nullable(), - image: objectWithId.nullable(), - inventoryQuantity: z.number().nullable(), - requiresShipping: z.boolean().nullable(), - position: z.number().nullable(), - taxCode: z.string().nullable(), - taxable: z.boolean().nullable(), - weight: z.number().nullable(), - weightUnit: z.string().nullable(), -}); - -export const SearchVariantsBodySchema = FirstOrLastSchema.and( - z.object({ - filter: z - .object({ - productId: z.array(z.string().transform((s) => parseGid(s))).optional(), - sku: z.array(z.string()).optional(), - }) - .optional(), - }) -); - -export const SearchVariantsSuccessResponseSchema = z.object({ - count: z.number(), - productVariants: z.array(ProductVariantSchema), -}); - -const InventoryQuantitySchema = z.object({ - availableQuantity: z.number(), - locationId: z.string(), -}); - -const InventoryItemSchema = z.object({ - cost: z.string().optional(), - tracked: z.boolean().optional(), -}); - -const InventoryPolicy = z.union([z.literal("CONTINUE"), z.literal("DENY")]); - -export const CreateVariantBodySchema = z.object({ - productId: z.string(), - options: z.array(z.string()), - price: z.string().optional(), - sku: z.string().optional(), - barcode: z.string().optional(), - compareAtPrice: z.string().optional(), - inventoryItem: InventoryItemSchema.optional(), - inventoryPolicy: InventoryPolicy.optional(), - inventoryQuantities: z.array(InventoryQuantitySchema).optional(), - requiresShipping: z.boolean().optional(), - position: z.number().optional(), - taxCode: z.string().optional(), - taxable: z.boolean().optional(), - weight: z.number().optional(), - weightUnit: z.string().optional(), -}); - -export const GetProductBodySchema = z.object({ id: z.string() }); - -export const CreateProductBodySchema = z.object({ - title: z.string(), - collectionsToJoin: z.array(z.string()).optional(), - customProductType: z.string().optional(), - descriptionHtml: z.string().optional(), - giftCard: z.boolean().optional(), - giftCardTemplateSuffix: z.string().optional(), - handle: z.string().optional(), - options: z.array(z.string()).optional(), - productType: z.string().optional(), - requiresSellingPlan: z.boolean().optional(), - seo: z - .object({ - title: z.string().optional(), - description: z.string().optional(), - }) - .optional(), - standardizedProductType: z - .object({ - productTaxonomyNodeId: z.string(), - }) - .optional(), - status: ProductStatusSchema.optional(), - tags: z.array(z.string()).optional(), - templateSuffix: z.string().optional(), - variants: z - .array(CreateVariantBodySchema.omit({ productId: true })) - .optional(), - vendor: z.string().optional(), -}); - -export const UpdateProductBodySchema = z.object({ id: z.string() }).and( - z.object({ - ...CreateProductBodySchema.shape, - title: z.string().optional(), - }) -); - -export const UpdateProductResponseSchema = ProductSchema; - -export const AppendProductImagesBodySchema = z.object({ - id: z.string(), - images: z.array( - z.object({ src: z.string(), altText: z.string().optional() }) - ), -}); - -export const ImageSchema = z.object({ - id: z.string(), - url: z.string(), - width: z.number(), - height: z.number(), - altText: z.string().optional(), -}); - -export const AppendProductImagesResponseSchema = z.array(ImageSchema); - -export const ListCollectionsBodySchema = FirstOrLastSchema.and( - z.object({ - filter: z - .object({ - title: z.array(z.string()).optional(), - collectionType: z.array(z.string()).optional(), - publishedStatus: z.array(z.string()).optional(), - publishableStatus: z.array(z.string()).optional(), - updatedAt: z.array(z.string()).optional(), - }) - .optional(), - }) -); -export const ListCollectionsResponseSchema = z.array( - z.object({ - id: z.string(), - title: z.string(), - handle: z.string(), - updatedAt: z.string(), - productsCount: z.number(), - sortOrder: z.string(), - }) -); - -export const ListLocationsBodySchema = FirstOrLastSchema; -export const ListLocationsResponseSchema = z.array( - z.object({ - id: z.string(), - name: z.string(), - isActive: z.boolean(), - }) -); - -export const AddProductsToCollectionBodySchema = z.object({ - collectionId: z.string(), - productIds: z.array(z.string()), -}); - -export const AddProductsToCollectionResponseSchema = z.object({ - collection: z.object({ - id: z.string(), - title: z.string(), - productsCount: z.number(), - }), -}); diff --git a/packages/trigger-providers/src/providers/slack/blocks.ts b/packages/trigger-providers/src/providers/slack/blocks.ts deleted file mode 100644 index 514b9725df9..00000000000 --- a/packages/trigger-providers/src/providers/slack/blocks.ts +++ /dev/null @@ -1,861 +0,0 @@ -import { z } from "zod"; - -export const imageElementSchema = z.object({ - type: z.literal("image"), - image_url: z.string(), - alt_text: z.string(), -}); - -export const plainTextElementSchema = z.object({ - type: z.literal("plain_text"), - text: z.string(), - emoji: z.boolean().optional(), -}); - -export const mrkdwnElementSchema = z.object({ - type: z.literal("mrkdwn"), - text: z.string(), - verbatim: z.boolean().optional(), -}); - -export const mrkdwnOptionSchema = z.object({ - text: mrkdwnElementSchema, - value: z.string().optional(), - url: z.string().optional(), - description: plainTextElementSchema.optional(), -}); - -export const plainTextOptionSchema = z.object({ - text: plainTextElementSchema, - value: z.string().optional(), - url: z.string().optional(), - description: plainTextElementSchema.optional(), -}); - -export const optionSchema = z.union([ - mrkdwnOptionSchema, - plainTextOptionSchema, -]); - -export const confirmSchema = z.object({ - title: plainTextElementSchema.optional(), - text: z.discriminatedUnion("type", [ - plainTextElementSchema, - mrkdwnElementSchema, - ]), - confirm: plainTextElementSchema.optional(), - deny: plainTextElementSchema.optional(), - style: z.union([z.literal("primary"), z.literal("danger")]).optional(), -}); - -/** - * @description Determines when an input element will return a - * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` interaction payload}. - */ -export const dispatchActionConfigSchema = z.object({ - /** - * @description An array of interaction types that you would like to receive a - * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload} for. Should be - * one or both of: - * `on_enter_pressed` — payload is dispatched when user presses the enter key while the input is in focus. Hint - * text will appear underneath the input explaining to the user to press enter to submit. - * `on_character_entered` — payload is dispatched when a character is entered (or removed) in the input. - */ - trigger_actions_on: z - .array( - z.union([ - z.literal("on_enter_pressed"), - z.literal("on_character_entered"), - ]) - ) - .optional(), -}); - -export const actionSchema = z.object({ - type: z.string(), - /** - * @description: An identifier for this action. You can use this when you receive an interaction payload to - * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique - * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. - */ - action_id: z.string().optional(), -}); - -export const actionIdSchema = z.object({ - /** - * @description: An identifier for this action. You can use this when you receive an interaction payload to - * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique - * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters. - */ - action_id: z.string().optional(), -}); - -export const confirmableSchema = z.object({ - /** - * @description A {@see Confirm} object that defines an optional confirmation dialog after the element is interacted - * with. - */ - confirm: confirmSchema.optional(), -}); - -export const focusableSchema = z.object({ - /** - * @description Indicates whether the element will be set to auto focus within the - * {@link https://api.slack.com/reference/surfaces/views `view` object}. Only one element can be set to `true`. - * Defaults to `false`. - */ - focus_on_load: z.boolean().optional(), -}); - -export const placeholdableSchema = z.object({ - /** - * @description A {@see PlainTextElement} object that defines the placeholder text shown on the element. Maximum - * length for the `text` field in this object is 150 characters. - */ - placeholder: plainTextElementSchema.optional(), -}); - -export const dispatchableSchema = z.object({ - /** - * @description A {@see DispatchActionConfig} object that determines when during text input the element returns a - * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload}. - */ - dispatch_action_config: dispatchActionConfigSchema.optional(), -}); - -export const usersSelectSchema = z - .object({ - type: z.literal("users_select"), - initial_user: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiUsersSelectSchema = z - .object({ - type: z.literal("multi_users_select"), - initial_users: z.array(z.string()).optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const staticSelectSchema = z - .object({ - type: z.literal("static_select"), - initial_option: plainTextOptionSchema.optional(), - options: z.array(plainTextOptionSchema).optional(), - option_groups: z - .array( - z.object({ - label: plainTextElementSchema, - options: z.array(plainTextOptionSchema), - }) - ) - .optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiStaticSelectSchema = z - .object({ - type: z.literal("multi_static_select"), - initial_options: z.array(plainTextOptionSchema).optional(), - options: z.array(plainTextOptionSchema).optional(), - option_groups: z - .array( - z.object({ - label: plainTextElementSchema, - options: z.array(plainTextOptionSchema), - }) - ) - .optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const conversationsSelectSchema = z - .object({ - type: z.literal("conversations_select"), - initial_conversation: z.string().optional(), - response_url_enabled: z.boolean().optional(), - default_to_current_conversation: z.boolean().optional(), - filter: z - .object({ - include: z - .array( - z.union([ - z.literal("im"), - z.literal("mpim"), - z.literal("private"), - z.literal("public"), - ]) - ) - .optional(), - exclude_external_shared_channels: z.boolean().optional(), - exclude_bot_users: z.boolean().optional(), - }) - .optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiConversationsSelectSchema = z - .object({ - type: z.literal("multi_conversations_select"), - initial_conversations: z.array(z.string()).optional(), - max_selected_items: z.number().optional(), - default_to_current_conversation: z.boolean().optional(), - filter: z - .object({ - include: z - .array( - z.union([ - z.literal("im"), - z.literal("mpim"), - z.literal("private"), - z.literal("public"), - ]) - ) - .optional(), - exclude_external_shared_channels: z.boolean().optional(), - exclude_bot_users: z.boolean().optional(), - }) - .optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const channelsSelectSchema = z - .object({ - type: z.literal("channels_select"), - initial_channel: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiChannelsSelectSchema = z - .object({ - type: z.literal("multi_channels_select"), - initial_channels: z.array(z.string()).optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const externalSelectSchema = z - .object({ - type: z.literal("external_select"), - initial_option: plainTextOptionSchema.optional(), - min_query_length: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const multiExternalSelectSchema = z - .object({ - type: z.literal("multi_external_select"), - initial_options: z.array(plainTextOptionSchema).optional(), - min_query_length: z.number().optional(), - max_selected_items: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const buttonSchema = z - .object({ - type: z.literal("button"), - text: plainTextElementSchema, - value: z.string().optional(), - url: z.string().optional(), - style: z.union([z.literal("danger"), z.literal("primary")]).optional(), - accessibility_label: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema); - -export const overflowSchema = z - .object({ - type: z.literal("overflow"), - options: z.array(plainTextOptionSchema), - }) - .merge(actionIdSchema) - .merge(confirmableSchema); - -export const datepickerSchema = z - .object({ - type: z.literal("datepicker"), - initial_date: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const timepickerSchema = z - .object({ - type: z.literal("timepicker"), - initial_time: z.string().optional(), - timezone: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const radioButtonsSchema = z - .object({ - type: z.literal("radio_buttons"), - initial_option: optionSchema.optional(), - options: z.array(optionSchema), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema); - -/** - * @description An element that allows the selection of a time of day formatted as a UNIX timestamp. On desktop - * clients, this time picker will take the form of a dropdown list merge the date picker will take the form of a dropdown - * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker merge date - * picker will use native UIs. - * {@link https://api.slack.com/reference/block-kit/block-elements#datetimepicker} - */ -export const dateTimepickerSchema = z - .object({ - type: z.literal("datetimepicker"), - /** - * @description The initial date merge time that is selected when the element is loaded, represented as a UNIX - * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date merge - * time August 10th, 2021 at 03:17pm PST. - */ - initial_date_time: z.number().optional(), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema); - -export const checkboxesSchema = z - .object({ - type: z.literal("checkboxes"), - initial_options: z.array(optionSchema).optional(), - options: z.array(optionSchema), - }) - .merge(actionIdSchema) - .merge(confirmableSchema) - .merge(focusableSchema); - -export const plainTextInputSchema = z - .object({ - type: z.literal("plain_text_input"), - initial_value: z.string().optional(), - multiline: z.boolean().optional(), - min_length: z.number().optional(), - max_length: z.number().optional(), - dispatch_action_config: dispatchActionConfigSchema.optional(), - focus_on_load: z.boolean().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -/** - * @description A URL input element, similar to the {@see PlainTextInput} element, creates a single line field where - * a user can enter URL-encoded data. - * {@link https://api.slack.com/reference/block-kit/block-elements#url} - */ -export const uRLInputSchema = z - .object({ - type: z.literal("url_text_input"), - /** - * @description The initial value in the URL input when it is loaded. - */ - initial_value: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -/** - * @description An email input element, similar to the {@see PlainTextInput} element, creates a single line field where - * a user can enter an email address. - * {@link https://api.slack.com/reference/block-kit/block-elements#email} - */ -export const emailInputSchema = z - .object({ - type: z.literal("email_text_input"), - /** - * @description The initial value in the email input when it is loaded. - */ - initial_value: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -/** - * @description A number input element, similar to the {@see PlainTextInput} element, creates a single line field where - * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, merge -10 are all - * valid input values. Decimal numbers are only allowed when `is_decimal_allowed` is equal to `true`. - * {@link https://api.slack.com/reference/block-kit/block-elements#number} - */ -export const numberInputSchema = z - .object({ - type: z.literal("number_input"), - /** - * @description Decimal numbers are allowed if this property is `true`, set the value to `false` otherwise. - */ - is_decimal_allowed: z.boolean(), - /** - * @description The initial value in the input when it is loaded. - */ - initial_value: z.string().optional(), - /** - * @description The minimum value, cannot be greater than `max_value`. - */ - min_value: z.string().optional(), - /** - * @description The maximum value, cannot be less than `min_value`. - */ - max_value: z.string().optional(), - }) - .merge(actionIdSchema) - .merge(dispatchableSchema) - .merge(focusableSchema) - .merge(placeholdableSchema); - -export const blockSchema = z.object({ - type: z.string(), - block_id: z.string().optional(), -}); - -export const imageBlockSchema = blockSchema.extend({ - type: z.literal("image"), - image_url: z.string(), - alt_text: z.string(), - title: plainTextElementSchema.optional(), -}); - -export const contextBlockSchema = blockSchema.extend({ - type: z.literal("context"), - elements: z.array( - z.discriminatedUnion("type", [ - imageElementSchema, - plainTextElementSchema, - mrkdwnElementSchema, - ]) - ), -}); - -export const dividerBlockSchema = blockSchema.extend({ - type: z.literal("divider"), -}); - -export const fileBlockSchema = blockSchema.extend({ - type: z.literal("file"), - source: z.string(), - external_id: z.string(), -}); - -export const headerBlockSchema = blockSchema.extend({ - type: z.literal("header"), - text: plainTextElementSchema, -}); - -export const messageMetadataEventPayloadObjectSchema = z.record( - z.union([z.string(), z.number(), z.boolean()]) -); - -export const messageAttachmentPreviewSchema = z.object({ - type: z.string().optional(), - can_remove: z.boolean().optional(), - title: plainTextElementSchema.optional(), - subtitle: plainTextElementSchema.optional(), - iconUrl: z.string().optional(), -}); - -export const optionFieldSchema = z.object({ - description: z.string().optional(), - text: z.string(), - value: z.string(), -}); - -export const confirmationSchema = z.object({ - dismiss_text: z.string().optional(), - ok_text: z.string().optional(), - text: z.string(), - title: z.string().optional(), -}); - -export const selectOptionSchema = z.object({ - label: z.string(), - value: z.string(), -}); - -export const callUserSlackSchema = z.object({ - slack_id: z.string(), -}); - -export const callUserExternalSchema = z.object({ - external_id: z.string(), - display_name: z.string(), - avatar_url: z.string(), -}); - -export const videoBlockSchema = blockSchema.extend({ - type: z.literal("video"), - video_url: z.string(), - thumbnail_url: z.string(), - alt_text: z.string(), - title: plainTextElementSchema, - title_url: z.string().optional(), - author_name: z.string().optional(), - provider_name: z.string().optional(), - provider_icon_url: z.string().optional(), - description: plainTextElementSchema.optional(), -}); - -export const dialogSchema = z.object({ - title: z.string(), - callback_id: z.string(), - elements: z.array( - z.object({ - type: z.union([ - z.literal("text"), - z.literal("textarea"), - z.literal("select"), - ]), - name: z.string(), - label: z.string(), - optional: z.boolean().optional(), - placeholder: z.string().optional(), - value: z.string().optional(), - max_length: z.number().optional(), - min_length: z.number().optional(), - hint: z.string().optional(), - subtype: z - .union([ - z.literal("email"), - z.literal("number"), - z.literal("tel"), - z.literal("url"), - ]) - .optional(), - data_source: z - .union([ - z.literal("users"), - z.literal("channels"), - z.literal("conversations"), - z.literal("external"), - ]) - .optional(), - selected_options: z.array(selectOptionSchema).optional(), - options: z.array(selectOptionSchema).optional(), - option_groups: z - .array( - z.object({ - label: z.string(), - options: z.array(selectOptionSchema), - }) - ) - .optional(), - min_query_length: z.number().optional(), - }) - ), - submit_label: z.string().optional(), - notify_on_cancel: z.boolean().optional(), - state: z.string().optional(), -}); - -const selectSchemas = [ - usersSelectSchema, - staticSelectSchema, - conversationsSelectSchema, - channelsSelectSchema, - externalSelectSchema, -]; - -export const selectSchema = z.discriminatedUnion("type", [ - usersSelectSchema, - staticSelectSchema, - conversationsSelectSchema, - channelsSelectSchema, - externalSelectSchema, -]); - -const multiSelectSchemas = [ - multiUsersSelectSchema, - multiStaticSelectSchema, - multiConversationsSelectSchema, - multiChannelsSelectSchema, - multiExternalSelectSchema, -]; -export const multiSelectSchema = z.discriminatedUnion("type", [ - multiUsersSelectSchema, - multiStaticSelectSchema, - multiConversationsSelectSchema, - multiChannelsSelectSchema, - multiExternalSelectSchema, -]); - -export const actionsBlockSchema = blockSchema.extend({ - type: z.literal("actions"), - elements: z.array( - z.discriminatedUnion("type", [ - buttonSchema, - overflowSchema, - datepickerSchema, - timepickerSchema, - dateTimepickerSchema, - ...selectSchemas, - radioButtonsSchema, - checkboxesSchema, - ]) - ), -}); - -export const sectionBlockSchema = blockSchema.extend({ - type: z.literal("section"), - text: z - .discriminatedUnion("type", [plainTextElementSchema, mrkdwnElementSchema]) - .optional(), - fields: z - .array( - z.discriminatedUnion("type", [ - plainTextElementSchema, - mrkdwnElementSchema, - ]) - ) - .optional(), - accessory: z - .discriminatedUnion("type", [ - buttonSchema, - overflowSchema, - datepickerSchema, - timepickerSchema, - ...selectSchemas, - ...multiSelectSchemas, - imageElementSchema, - radioButtonsSchema, - checkboxesSchema, - ]) - .optional(), -}); - -export const inputBlockSchema = blockSchema.extend({ - type: z.literal("input"), - label: plainTextElementSchema, - hint: plainTextElementSchema.optional(), - optional: z.boolean().optional(), - element: z.discriminatedUnion("type", [ - datepickerSchema, - ...selectSchemas, - ...multiSelectSchemas, - timepickerSchema, - dateTimepickerSchema, - plainTextInputSchema, - uRLInputSchema, - emailInputSchema, - numberInputSchema, - radioButtonsSchema, - checkboxesSchema, - ]), - dispatch_action: z.boolean().optional(), -}); - -export const messageMetadataSchema = z.object({ - event_type: z.string(), - event_payload: z.record( - z.union([ - z.string(), - z.number(), - z.boolean(), - messageMetadataEventPayloadObjectSchema, - z.array(messageMetadataEventPayloadObjectSchema), - ]) - ), -}); - -export const attachmentActionSchema = z.object({ - id: z.string().optional(), - confirm: confirmationSchema.optional(), - data_source: z - .union([ - z.literal("static"), - z.literal("channels"), - z.literal("conversations"), - z.literal("users"), - z.literal("external"), - ]) - .optional(), - min_query_length: z.number().optional(), - name: z.string().optional(), - options: z.array(optionFieldSchema).optional(), - option_groups: z - .array( - z.object({ - text: z.string(), - options: z.array(optionFieldSchema), - }) - ) - .optional(), - selected_options: z.array(optionFieldSchema).optional(), - style: z - .union([z.literal("default"), z.literal("primary"), z.literal("danger")]) - .optional(), - text: z.string(), - type: z.union([z.literal("button"), z.literal("select")]), - value: z.string().optional(), - url: z.string().optional(), -}); - -export const callUserSchema = z.union([ - callUserSlackSchema, - callUserExternalSchema, -]); - -const knownBlocks = [ - imageBlockSchema, - contextBlockSchema, - actionsBlockSchema, - dividerBlockSchema, - sectionBlockSchema, - inputBlockSchema, - fileBlockSchema, - headerBlockSchema, - videoBlockSchema, -]; - -export const knownBlockSchema = z.discriminatedUnion("type", [ - imageBlockSchema, - contextBlockSchema, - actionsBlockSchema, - dividerBlockSchema, - sectionBlockSchema, - inputBlockSchema, - fileBlockSchema, - headerBlockSchema, - videoBlockSchema, -]); - -const anyBlockSchema = z.discriminatedUnion("type", [ - imageBlockSchema, - contextBlockSchema, - actionsBlockSchema, - dividerBlockSchema, - sectionBlockSchema, - inputBlockSchema, - fileBlockSchema, - headerBlockSchema, - videoBlockSchema, -]); - -export const messageAttachmentSchema = z.object({ - blocks: z.array(anyBlockSchema).optional(), - fallback: z.string().optional(), - color: z - .union([ - z.literal("good"), - z.literal("warning"), - z.literal("danger"), - z.string(), - ]) - .optional(), - pretext: z.string().optional(), - author_name: z.string().optional(), - author_link: z.string().optional(), - author_icon: z.string().optional(), - title: z.string().optional(), - title_link: z.string().optional(), - text: z.string().optional(), - fields: z - .array( - z.object({ - title: z.string(), - value: z.string(), - short: z.boolean().optional(), - }) - ) - .optional(), - image_url: z.string().optional(), - thumb_url: z.string().optional(), - footer: z.string().optional(), - footer_icon: z.string().optional(), - ts: z.string().optional(), - actions: z.array(attachmentActionSchema).optional(), - callback_id: z.string().optional(), - mrkdwn_in: z - .array( - z.union([z.literal("pretext"), z.literal("text"), z.literal("fields")]) - ) - .optional(), - app_unfurl_url: z.string().optional(), - is_app_unfurl: z.boolean().optional(), - app_id: z.string().optional(), - bot_id: z.string().optional(), - preview: messageAttachmentPreviewSchema.optional(), -}); - -export const linkUnfurlsSchema = z.record(messageAttachmentSchema); - -export const homeViewSchema = z.object({ - type: z.literal("home"), - blocks: z.array(anyBlockSchema), - private_metadata: z.string().optional(), - callback_id: z.string().optional(), - external_id: z.string().optional(), -}); - -export const modalViewSchema = z.object({ - type: z.literal("modal"), - title: plainTextElementSchema, - blocks: z.array(anyBlockSchema), - close: plainTextElementSchema.optional(), - submit: plainTextElementSchema.optional(), - private_metadata: z.string().optional(), - callback_id: z.string().optional(), - clear_on_close: z.boolean().optional(), - notify_on_close: z.boolean().optional(), - external_id: z.string().optional(), -}); - -export const workflowStepViewSchema = z.object({ - type: z.literal("workflow_step"), - blocks: z.array(anyBlockSchema), - private_metadata: z.string().optional(), - callback_id: z.string().optional(), - submit_disabled: z.boolean().optional(), - external_id: z.string().optional(), -}); - -export const viewSchema: z.ZodDiscriminatedUnion< - "type", - [typeof homeViewSchema, typeof modalViewSchema, typeof workflowStepViewSchema] -> = z.discriminatedUnion("type", [ - homeViewSchema, - modalViewSchema, - workflowStepViewSchema, -]); diff --git a/packages/trigger-providers/src/providers/slack/index.ts b/packages/trigger-providers/src/providers/slack/index.ts deleted file mode 100644 index 51121adba63..00000000000 --- a/packages/trigger-providers/src/providers/slack/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as schemas from "./schemas"; - -export const slack = { - name: "Slack", - slug: "slack", - icon: "/integrations/slack.png", - enabledFor: "all", - authentication: { - type: "oauth", - scopes: [ - "channels:read", - "channels:join", - "channels:manage", - "chat:write", - "groups:write", - "im:write", - "mpim:write", - "chat:write.customize", - "reactions:write", - ], - }, - schemas, -}; diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts deleted file mode 100644 index 2e341d605e3..00000000000 --- a/packages/trigger-providers/src/providers/slack/interactivity.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { z } from "zod"; -import { - knownBlockSchema, - mrkdwnElementSchema, - optionFieldSchema, - plainTextElementSchema, - viewSchema, -} from "./blocks"; - -const textSchema = z.discriminatedUnion("type", [ - plainTextElementSchema, - mrkdwnElementSchema, -]); -const blockActionType = z.union([ - z.literal("block_actions"), - z.literal("interactive_message"), -]); - -const sourceType = z.literal("message"); - -const commonActionSchema = z.object({ - action_id: z.string(), - block_id: z.string(), - action_ts: z.string(), -}); - -const buttonAction = z.object({ - type: z.literal("button"), - text: textSchema.optional(), - value: z.string(), -}); - -const selectedOptionSchema = z.object({ - text: z.object({ - type: z.string(), - text: z.string(), - emoji: z.boolean().optional(), - }), - value: z.string(), -}); -const placeholderSchema = z.object({ - type: z.string(), - text: z.string(), - emoji: z.boolean(), -}); -const staticSelectAction = z.object({ - type: z.literal("static_select"), - selected_option: selectedOptionSchema.nullable(), - placeholder: placeholderSchema.optional(), -}); - -const userSelectAction = z.object({ - type: z.literal("users_select"), - selected_user: z.string().nullable(), - initial_user: z.string().optional(), -}); - -const conversationsSelectAction = z.object({ - type: z.literal("conversations_select"), - selected_conversation: z.string().nullable(), - initial_conversation: z.string().optional(), -}); -const channelSelectAction = z.object({ - type: z.literal("channels_select"), - selected_channel: z.string().nullable(), - initial_channel: z.string().optional(), -}); - -const datePickerAction = z.object({ - type: z.literal("datepicker"), - selected_date: z.string().nullable(), - initial_date: z.string().optional(), -}); - -const checkboxesAction = z.object({ - type: z.literal("checkboxes"), - selected_options: z.array(optionFieldSchema), -}); - -const radioButtonsSchema = z.object({ - type: z.literal("radio_buttons"), - selectedOption: optionFieldSchema, -}); - -const timePickerSchema = z.object({ - type: z.literal("timepicker"), - selected_time: z.string().nullable(), - initial_time: z.string().optional(), -}); - -const plainTextInputSchema = z.object({ - type: z.literal("plain_text_input"), - value: z.string().nullable(), - initial_value: z.string().optional(), -}); - -const multiUsersSelectSchema = z.object({ - type: z.literal("multi_users_select"), - selected_users: z.array(z.string()), - initial_users: z.array(z.string()).optional(), -}); - -const multiStaticSelectSchema = z.object({ - type: z.literal("multi_static_select"), - selected_options: z.array(selectedOptionSchema), - placeholder: placeholderSchema.optional(), -}); - -const possibleActionsSchema = z.discriminatedUnion("type", [ - buttonAction, - staticSelectAction, - userSelectAction, - conversationsSelectAction, - channelSelectAction, - datePickerAction, - checkboxesAction, - radioButtonsSchema, - timePickerSchema, - plainTextInputSchema, - multiUsersSelectSchema, - multiStaticSelectSchema, -]); - -const actionSchema = possibleActionsSchema.and(commonActionSchema); - -//state.values.issue.action.block.rating.selected_option -const stateSchema = z.object({ - values: z.record(z.record(possibleActionsSchema)), -}); - -const userSchema = z.object({ - id: z.string(), - username: z.string(), - name: z.string(), - team_id: z.string(), -}); - -const containerSchema = z.object({ - type: sourceType, - message_ts: z.string(), - channel_id: z.string(), - is_ephemeral: z.boolean(), -}); - -const teamSchema = z.object({ id: z.string(), domain: z.string() }); -const channelSchema = z.object({ id: z.string(), name: z.string() }); - -const viewActionDataSchema = z.object({ - id: z.string(), - team_id: z.string(), - state: z - .object({ - values: z.record(z.record(z.any())), - }) - .optional(), - hash: z.string(), - previous_view_id: z.string().optional(), - root_view_id: z.string().optional(), - app_id: z.string().optional(), - app_installed_team_id: z.string().optional(), - bot_id: z.string().optional(), -}); -const viewActionSchema: Zod.ZodIntersection< - typeof viewSchema, - typeof viewActionDataSchema -> = viewSchema.and(viewActionDataSchema); - -const messageActionSchema = z.object({ - bot_id: z.string(), - type: sourceType, - text: z.string().optional(), - user: z.string().optional(), - ts: z.string(), - app_id: z.string().optional(), - blocks: z.array(knownBlockSchema).optional(), - team: z.string().optional(), - metadata: z - .object({ - event_type: z.string(), - event_payload: z.object({ requestId: z.string() }), - }) - .optional(), -}); - -export const blockAction: Zod.ZodObject<{ - type: typeof blockActionType; - user: typeof userSchema; - api_app_id: Zod.ZodString; - container: typeof containerSchema; - trigger_id: z.ZodOptional; - team: typeof teamSchema; - enterprise: Zod.ZodAny; - is_enterprise_install: Zod.ZodBoolean; - channel: typeof channelSchema; - view: z.ZodOptional; - message: z.ZodOptional; - state: z.ZodOptional; - response_url: Zod.ZodString; - actions: Zod.ZodArray; -}> = z.object({ - type: blockActionType, - user: userSchema, - api_app_id: z.string(), - container: containerSchema, - trigger_id: z.string().optional(), - team: teamSchema, - enterprise: z.any(), - is_enterprise_install: z.boolean(), - channel: channelSchema, - view: viewActionSchema.optional(), - message: messageActionSchema.optional(), - state: stateSchema.optional(), - response_url: z.string(), - actions: z.array(actionSchema), -}); diff --git a/packages/trigger-providers/src/providers/slack/prodmanifest.json b/packages/trigger-providers/src/providers/slack/prodmanifest.json deleted file mode 100644 index 2f63a247e93..00000000000 --- a/packages/trigger-providers/src/providers/slack/prodmanifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "display_information": { - "name": "Trigger.dev" - }, - "features": { - "bot_user": { - "display_name": "Trigger.dev", - "always_online": false - } - }, - "oauth_config": { - "redirect_urls": [ - "https://auth.trigger.dev/oauth/callback" - ], - "scopes": { - "user": [ - "channels:read", - "groups:read", - "im:read", - "mpim:read", - "chat:write", - "reactions:write" - ], - "bot": [ - "chat:write", - "reactions:write" - ] - } - }, - "settings": { - "org_deploy_enabled": false, - "socket_mode_enabled": false, - "token_rotation_enabled": false - } -} \ No newline at end of file diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts deleted file mode 100644 index c1fe80518cd..00000000000 --- a/packages/trigger-providers/src/providers/slack/schemas.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { z } from "zod"; -import { knownBlockSchema } from "./blocks"; -import { blockAction } from "./interactivity"; - -export { blockAction }; - -export const PostMessageSuccessResponseSchema = z.object({ - ok: z.literal(true), - channel: z.string(), - ts: z.string(), - message: z.object({ - text: z.string(), - user: z.string().optional(), - bot_id: z.string(), - attachments: z.array(z.unknown()).optional(), - type: z.string(), - subtype: z.string().optional(), - ts: z.string(), - }), -}); - -export const ErrorResponseSchema = z.object({ - ok: z.literal(false), - error: z.string(), -}); - -export const PostMessageResponseSchema = z.discriminatedUnion("ok", [ - PostMessageSuccessResponseSchema, - ErrorResponseSchema, -]); - -export const PostMessageBodySchema = z.object({ - channel: z.string(), - text: z.string(), - blocks: z.array(knownBlockSchema).optional(), - username: z.string().optional(), - icon_emoji: z.string().optional(), - icon_url: z.string().optional(), -}); - -export const ChannelNameOrIdSchema = z.union([ - z.object({ channelId: z.string() }), - z.object({ channelName: z.string() }), -]); - -export const PostMessageOptionsSchema = z - .object({ - text: z.string(), - blocks: z.array(knownBlockSchema).optional(), - username: z.string().optional(), - icon_emoji: z.string().optional(), - icon_url: z.string().optional(), - }) - .and(ChannelNameOrIdSchema); - -export const AddReactionOptionsSchema = z - .object({ - name: z.string(), - timestamp: z.string(), - }) - .and(ChannelNameOrIdSchema); - -export const JoinConversationSuccessResponseSchema = z.object({ - ok: z.literal(true), - channel: z.object({ - id: z.string(), - }), -}); - -export const JoinConversationResponseSchema = z.discriminatedUnion("ok", [ - JoinConversationSuccessResponseSchema, - ErrorResponseSchema, -]); - -export const JoinConversationBodySchema = z.object({ - channel: z.string(), -}); - -export const ListConversationsSuccessResponseSchema = z.object({ - ok: z.literal(true), - channels: z.array( - z.object({ - id: z.string(), - name: z.string(), - }) - ), -}); - -export const ListConversationsResponseSchema = z.discriminatedUnion("ok", [ - ListConversationsSuccessResponseSchema, - ErrorResponseSchema, -]); - -export const PostMessageResponseOptionsSchema = z.object({ - text: z.string().optional(), - blocks: z.array(knownBlockSchema).optional(), - response_type: z.enum(["in_channel"]).optional(), - replace_original: z.boolean().optional(), - delete_original: z.boolean().optional(), - thread_ts: z.string().optional(), -}); - -export const PostMessageResponseSuccessResponseSchema = z.object({ - ok: z.literal(true), -}); - -export const PostMessageResponseResponseSchema = z.discriminatedUnion("ok", [ - PostMessageResponseSuccessResponseSchema, - ErrorResponseSchema, -]); - -export const AddReactionSuccessResponseSchema = z.object({ - ok: z.literal(true), -}); - -export const AddReactionResponseSchema = z.discriminatedUnion("ok", [ - AddReactionSuccessResponseSchema, - ErrorResponseSchema, -]); diff --git a/packages/trigger-providers/src/schemaUtils.ts b/packages/trigger-providers/src/schemaUtils.ts deleted file mode 100644 index 7d3ca62b851..00000000000 --- a/packages/trigger-providers/src/schemaUtils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { z } from "zod"; -import { CamelCasedPropertiesDeep, SnakeCasedPropertiesDeep } from "type-fest"; - -/** Converts a Zod Schema from snake_case to camelCase */ -export function snakeToCamel( - schema: z.ZodType -) { - return schema.transform( - (object) => - deepSnakeToCamel(object) as unknown as CamelCasedPropertiesDeep< - typeof object - > - ); -} - -/** Converts a Zod Schema from camelCase to snake_case */ -export function camelToSnake( - schema: z.ZodType -) { - return schema.transform( - (object) => - deepCamelToSnake(object) as unknown as SnakeCasedPropertiesDeep< - typeof object - > - ); -} - -function deepSnakeToCamel(o: any): T { - if (isObject(o)) { - const n = {}; - - Object.keys(o).forEach((k) => { - // @ts-ignore - n[keySnakeToCamel(k)] = deepSnakeToCamel(o[k]); - }); - - return n as T; - } else if (isArray(o)) { - return o.map((i: any) => { - return deepSnakeToCamel(i); - }); - } - - return o; -} - -function keySnakeToCamel(s: string): string { - return s.replace(/([_][a-z])/gi, ($1) => { - return $1.toUpperCase().replace("-", "").replace("_", ""); - }); -} - -function deepCamelToSnake(o: any): T { - if (isObject(o)) { - const n = {}; - - Object.keys(o).forEach((k) => { - // @ts-ignore - n[keyCamelToSnake(k)] = deepCamelToSnake(o[k]); - }); - - return n as T; - } else if (isArray(o)) { - return o.map((i: any) => { - return deepCamelToSnake(i); - }); - } - - return o; -} - -function keyCamelToSnake(s: string): string { - return s - .replace(/[\w]([A-Z])/g, function (m) { - return m[0] + "_" + m[1]; - }) - .toLowerCase(); -} - -function isArray(a: any): boolean { - return Array.isArray(a); -} - -function isObject(o: any): boolean { - return o === Object(o) && !isArray(o) && typeof o !== "function"; -} diff --git a/packages/trigger-providers/src/types.ts b/packages/trigger-providers/src/types.ts deleted file mode 100644 index b9b357af204..00000000000 --- a/packages/trigger-providers/src/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ZodTypeAny } from "zod"; - -export type Provider = { - name: string; - slug: string; - icon: string; - enabledFor: "all" | "admins" | "none"; - authentication: OAuthAuthentication | APIKeyAuthentication; - schemas: Record; -}; - -export type SerializableProvider = Omit; - -export type OAuthAuthentication = { - type: "oauth"; - scopes: string[]; -}; - -export type APIKeyAuthentication = { - type: "api_key"; - header_name: string; - header_type: "access_token" | "bearer"; - documentation: string; - additionalFields?: { - key: string; - fieldType: "text"; - name: string; - placeholder?: string; - description: string; - }[]; -}; - -export type ProviderCatalog = { - providers: Record; -}; diff --git a/packages/trigger-providers/tsconfig.json b/packages/trigger-providers/tsconfig.json deleted file mode 100644 index 2fae9036950..00000000000 --- a/packages/trigger-providers/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@trigger.dev/tsconfig/node16.json", - "include": ["./src/**/*.ts"], - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "paths": {}, - "lib": ["DOM"] - }, - "exclude": ["node_modules"] -} diff --git a/packages/trigger-providers/tsup.config.ts b/packages/trigger-providers/tsup.config.ts deleted file mode 100644 index 764c600c547..00000000000 --- a/packages/trigger-providers/tsup.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig([ - { - name: "main", - entry: ["./src/index.ts"], - outDir: "./dist", - platform: "node", - format: ["cjs"], - legacyOutput: true, - sourcemap: true, - clean: true, - bundle: true, - splitting: false, - dts: true, - external: [ - "http", - "https", - "util", - "events", - "tty", - "os", - "timers", - "react", - ], - esbuildPlugins: [], - }, -]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc5f2835847..6792d02c02e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,7 +77,9 @@ importers: '@trigger.dev/common-schemas': workspace:* '@trigger.dev/github': workspace:* '@trigger.dev/integration-sdk': workspace:* - '@trigger.dev/providers': workspace:* + '@trigger.dev/resend': workspace:* + '@trigger.dev/shopify': workspace:* + '@trigger.dev/slack': workspace:* '@trigger.dev/tailwind-config': workspace:* '@typeform/embed-react': ^2.14.1 '@types/bcryptjs': ^2.4.2 @@ -125,8 +127,8 @@ importers: glob: ^8.0.3 happy-dom: ^6.0.4 humanize-duration: ^3.27.3 + integration-catalog: workspace:* internal-bridge: workspace:* - internal-integrations: workspace:* internal-platform: workspace:* internal-pulsar: workspace:* ioredis: ^5.2.4 @@ -211,7 +213,9 @@ importers: '@tanstack/react-table': 8.7.6_biqbaboplfbrettd7655fr4n2y '@trigger.dev/common-schemas': link:../../packages/common-schemas '@trigger.dev/github': link:../../integrations/github - '@trigger.dev/providers': link:../../packages/trigger-providers + '@trigger.dev/resend': link:../../integrations/resend + '@trigger.dev/shopify': link:../../integrations/shopify + '@trigger.dev/slack': link:../../integrations/slack '@typeform/embed-react': 2.14.1_react@18.2.0 '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle bcryptjs: 2.4.3 @@ -227,8 +231,8 @@ importers: emails: link:../../packages/emails express: 4.18.2 humanize-duration: 3.27.3 + integration-catalog: link:../../packages/integration-catalog internal-bridge: link:../../packages/internal-bridge - internal-integrations: link:../../packages/internal-integrations internal-platform: link:../../packages/internal-platform internal-pulsar: link:../../packages/internal-pulsar ioredis: 5.2.4 @@ -427,14 +431,12 @@ importers: examples/playground: specifiers: - '@trigger.dev/integrations': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' tsx: ^3.12.0 zod: ^3.20.2 dependencies: - '@trigger.dev/integrations': link:../../packages/trigger-integrations '@trigger.dev/sdk': link:../../packages/trigger-sdk zod: 3.20.2 devDependencies: @@ -721,12 +723,29 @@ importers: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/react': 18.0.26 + packages/integration-catalog: + specifiers: + '@trigger.dev/github': workspace:* + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/resend': workspace:* + '@trigger.dev/shopify': workspace:* + '@trigger.dev/slack': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/node': '16' + devDependencies: + '@trigger.dev/github': link:../../integrations/github + '@trigger.dev/integration-sdk': link:../integration-sdk + '@trigger.dev/resend': link:../../integrations/resend + '@trigger.dev/shopify': link:../../integrations/shopify + '@trigger.dev/slack': link:../../integrations/slack + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + packages/integration-sdk: specifiers: '@trigger.dev/tsconfig': workspace:* '@types/debug': ^4.1.7 '@types/node': '16' - '@types/react': ^18.0.21 debug: ^4.3.4 rimraf: ^3.0.2 tsup: ^6.5.0 @@ -738,7 +757,6 @@ importers: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/debug': 4.1.7 '@types/node': 16.18.11 - '@types/react': 18.0.26 rimraf: 3.0.2 tsup: 6.5.0 @@ -775,11 +793,11 @@ importers: specifiers: '@aws-sdk/client-secrets-manager': ^3.238.0 '@aws-sdk/credential-providers': ^3.238.0 - '@trigger.dev/providers': workspace:* '@trigger.dev/tsconfig': workspace:* '@types/node': '16' '@types/node-fetch': '2.6' commander: ^9.4.1 + integration-catalog: workspace:* node-fetch: '2.6' tiny-invariant: ^1.2.0 tsx: ^3.12.0 @@ -789,50 +807,15 @@ importers: devDependencies: '@aws-sdk/client-secrets-manager': 3.245.0 '@aws-sdk/credential-providers': 3.245.0 - '@trigger.dev/providers': link:../trigger-providers '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 16.18.11 '@types/node-fetch': 2.6.2 commander: 9.5.0 + integration-catalog: link:../integration-catalog tiny-invariant: 1.3.1 tsx: 3.12.2 zod: 3.20.2 - packages/internal-integrations: - specifiers: - '@octokit/webhooks': ^10.4.0 - '@shopify/admin-graphql-api-utilities': ^2.0.1 - '@shopify/shopify-api': ^6.1.0 - '@trigger.dev/providers': workspace:* - '@trigger.dev/tsconfig': workspace:* - '@types/debug': ^4.1.7 - '@types/node': '16' - '@types/react': ^18.0.21 - '@urql/core': ^3.1.1 - debug: ^4.3.4 - graphql: ^16.6.0 - react: ^18.2.0 - typescript: ^4.9.4 - urql: ^3.0.3 - zod: ^3.20.2 - dependencies: - '@octokit/webhooks': 10.5.1 - '@shopify/admin-graphql-api-utilities': 2.0.1 - '@shopify/shopify-api': 6.1.0 - '@urql/core': 3.1.1_graphql@16.6.0 - debug: 4.3.4 - graphql: 16.6.0 - react: 18.2.0 - urql: 3.0.3_onqnqwb3ubg5opvemcqf7c2qhy - zod: 3.20.2 - devDependencies: - '@trigger.dev/providers': link:../trigger-providers - '@trigger.dev/tsconfig': link:../../config-packages/tsconfig - '@types/debug': 4.1.7 - '@types/node': 16.18.11 - '@types/react': 18.0.26 - typescript: 4.9.4 - packages/internal-platform: specifiers: '@trigger.dev/common-schemas': workspace:* @@ -868,52 +851,6 @@ importers: '@types/node': 16.18.11 typescript: 4.9.4 - packages/trigger-integrations: - specifiers: - '@react-email/render': ^0.0.3 - '@trigger.dev/providers': workspace:* - '@trigger.dev/sdk': workspace:* - '@trigger.dev/tsconfig': workspace:* - '@types/node': '16' - rimraf: ^3.0.2 - tsup: ^6.5.0 - zod: ^3.20.2 - dependencies: - '@react-email/render': 0.0.3 - '@trigger.dev/providers': link:../trigger-providers - '@trigger.dev/sdk': link:../trigger-sdk - zod: 3.20.2 - devDependencies: - '@trigger.dev/tsconfig': link:../../config-packages/tsconfig - '@types/node': 16.18.11 - rimraf: 3.0.2 - tsup: 6.5.0 - - packages/trigger-providers: - specifiers: - '@shopify/admin-graphql-api-utilities': ^2.0.1 - '@trigger.dev/tsconfig': workspace:* - '@types/node': '16' - '@types/react': ^18.0.26 - rimraf: ^3.0.2 - tiny-invariant: ^1.2.0 - tsup: ^6.5.0 - type-fest: ^3.5.3 - typescript: ^4.9.4 - zod: ^3.20.2 - dependencies: - '@shopify/admin-graphql-api-utilities': 2.0.1 - tiny-invariant: 1.3.1 - zod: 3.20.2 - devDependencies: - '@trigger.dev/tsconfig': link:../../config-packages/tsconfig - '@types/node': 16.18.11 - '@types/react': 18.0.26 - rimraf: 3.0.2 - tsup: 6.5.0_typescript@4.9.4 - type-fest: 3.5.3 - typescript: 4.9.4 - packages/trigger-sdk: specifiers: '@trigger.dev/common-schemas': workspace:* @@ -4593,6 +4530,7 @@ packages: /@octokit/openapi-types/14.0.0: resolution: {integrity: sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==} + dev: true /@octokit/openapi-types/16.0.0: resolution: {integrity: sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA==} @@ -4634,6 +4572,7 @@ packages: '@octokit/types': 8.1.0 deprecation: 2.3.1 once: 1.4.0 + dev: true /@octokit/request/6.2.3: resolution: {integrity: sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==} @@ -4665,6 +4604,7 @@ packages: resolution: {integrity: sha512-N4nLjzkiWBqVQqljTTsCrbvHGoWdWfcCeZjbHdggw7a9HbJMnxbK8A+UWdqwR4out30JarlSa3eqKyVK0n5aBg==} dependencies: '@octokit/openapi-types': 14.0.0 + dev: true /@octokit/types/9.0.0: resolution: {integrity: sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==} @@ -4675,9 +4615,11 @@ packages: /@octokit/webhooks-methods/3.0.2: resolution: {integrity: sha512-Vlnv5WBscf07tyAvfDbp7pTkMZUwk7z7VwEF32x6HqI+55QRwBTcT+D7DDjZXtad/1dU9E32x0HmtDlF9VIRaQ==} engines: {node: '>= 14'} + dev: true /@octokit/webhooks-types/6.7.0: resolution: {integrity: sha512-bykm7UkSnxmb2uhSfcLM1Pity/LQ6ZBSdzy9HU0vXjR+2g+tzlmRhXb7Go8oj0TlgO+vDrTivGXju6zkzOGKjA==} + dev: true /@octokit/webhooks/10.5.1: resolution: {integrity: sha512-6yGcfVbQGIkRm/vlU4Ld4r0YNOT6ly5Kp+t6R3UAT0amxGASFtZLzOJry0UQfvvidCxK5qT9jHXU0QUp6alatA==} @@ -4687,6 +4629,7 @@ packages: '@octokit/webhooks-methods': 3.0.2 '@octokit/webhooks-types': 6.7.0 aggregate-error: 3.1.0 + dev: true /@open-draft/until/1.0.3: resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} @@ -5247,6 +5190,7 @@ packages: /@shopify/network/1.6.4: resolution: {integrity: sha512-V+//Et386LnYdtNhQ3e33AKYfU25XEt8H5XYeMqPvJZpVvC9Z1lHKQMpmM/zq13VXjPUjt9/sNxHxMP3I6cbJg==} + dev: true /@shopify/shopify-api/6.1.0: resolution: {integrity: sha512-XUyeqqRLsp8L24allGlezw2ILQ9ej9LvWS0A6GIE6k4Ku3fQDnSDIOVI2LRRkrZG92ZO3CLoM0Lblq0K9Z8RMg==} @@ -5261,6 +5205,7 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - encoding + dev: true /@sideway/address/4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} @@ -5607,6 +5552,7 @@ packages: /@types/cookiejar/2.1.2: resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} + dev: true /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} @@ -5775,6 +5721,7 @@ packages: dependencies: '@types/node': 18.11.18 form-data: 3.0.1 + dev: true /@types/node/10.17.60: resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} @@ -5889,11 +5836,13 @@ packages: dependencies: '@types/cookiejar': 2.1.2 '@types/node': 18.11.18 + dev: true /@types/supertest/2.0.12: resolution: {integrity: sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==} dependencies: '@types/superagent': 4.1.16 + dev: true /@types/testing-library__jest-dom/5.14.5: resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} @@ -6111,6 +6060,7 @@ packages: dependencies: graphql: 16.6.0 wonka: 6.1.2 + dev: true /@vitejs/plugin-react/2.2.0_vite@3.2.5: resolution: {integrity: sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==} @@ -8087,6 +8037,7 @@ packages: /deprecation/2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: true /dequal/2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} @@ -9700,6 +9651,7 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + dev: true /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -10160,6 +10112,7 @@ packages: /graphql/16.6.0: resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: true /gray-matter/4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} @@ -11142,6 +11095,7 @@ packages: /jose/4.11.2: resolution: {integrity: sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==} + dev: true /joycon/3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -11673,6 +11627,7 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 + dev: true /lru_map/0.3.3: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} @@ -14752,6 +14707,7 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true /send/0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -16030,11 +15986,6 @@ packages: engines: {node: '>=14.16'} dev: true - /type-fest/3.5.3: - resolution: {integrity: sha512-V2+og4j/rWReWvaFrse3s9g2xvUv/K9Azm/xo6CjIuq7oeGqsoimC7+9/A3tfvNcbQf8RPSVj/HV81fB4DJrjA==} - engines: {node: '>=14.16'} - dev: true - /type-is/1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -16287,6 +16238,7 @@ packages: graphql: 16.6.0 react: 18.2.0 wonka: 6.1.2 + dev: true /use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} @@ -16670,6 +16622,7 @@ packages: /wonka/6.1.2: resolution: {integrity: sha512-zNrXPMccg/7OEp9tSfFkMgTvhhowqasiSHdJ3eCZolXxVTV/aT6HUTofoZk9gwRbGoFey/Nss3JaZKUMKMbofg==} + dev: true /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} @@ -16818,6 +16771,7 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} From 89d38c04aa0e902045f07184e67e11cc6473fe84 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 16:10:43 +0000 Subject: [PATCH 33/71] Added changesets for the new packages --- .changeset/config.json | 10 +++++++--- .changeset/fresh-eyes-run.md | 5 +++++ .changeset/honest-dolls-return.md | 8 ++++++++ CHANGESETS.md | 7 +++++-- flightcontrol.json | 5 ++--- package.json | 3 ++- packages/integration-catalog/package.json | 1 + packages/internal-cli/tsconfig.json | 4 ++-- 8 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 .changeset/fresh-eyes-run.md create mode 100644 .changeset/honest-dolls-return.md diff --git a/.changeset/config.json b/.changeset/config.json index e12d5ab6e39..5c191e9bfbd 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,8 +4,12 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["wss", "webapp", "docs"] -} + "ignore": [ + "wss", + "webapp", + "docs" + ] +} \ No newline at end of file diff --git a/.changeset/fresh-eyes-run.md b/.changeset/fresh-eyes-run.md new file mode 100644 index 00000000000..a99234fa3e5 --- /dev/null +++ b/.changeset/fresh-eyes-run.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/integration-sdk": patch +--- + +Created new @trigger.dev/integration-sdk to support our integration packages diff --git a/.changeset/honest-dolls-return.md b/.changeset/honest-dolls-return.md new file mode 100644 index 00000000000..013bbae4f3c --- /dev/null +++ b/.changeset/honest-dolls-return.md @@ -0,0 +1,8 @@ +--- +"@trigger.dev/shopify": patch +"@trigger.dev/github": patch +"@trigger.dev/resend": patch +"@trigger.dev/slack": patch +--- + +Initial release of isolated integration packages diff --git a/CHANGESETS.md b/CHANGESETS.md index c22c2456f8d..27d69619db6 100644 --- a/CHANGESETS.md +++ b/CHANGESETS.md @@ -7,8 +7,11 @@ Trigger.dev uses [changesets](https://github.com/changesets/changesets) to manag To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset, which currently are: - `@trigger.dev/sdk` -- `@trigger.dev/integrations` -- `@trigger.dev/providers` +- `@trigger.dev/integration-sdk` +- `@trigger.dev/github` +- `@trigger.dev/slack` +- `@trigger.dev/shopify` +- `@trigger.dev/resend` ## Release instructions diff --git a/flightcontrol.json b/flightcontrol.json index b2f3d87451a..052956e39c0 100644 --- a/flightcontrol.json +++ b/flightcontrol.json @@ -55,8 +55,8 @@ "apps/webapp/types/**", "packages/internal-platform/src/**", "packages/common-schemas/src/**", - "packages/internal-integrations/src/**", - "packages/trigger-providers/src/**", + "integrations/*/src/**", + "packages/integration-catalog/src/**", "packages/emails/src/**", "packages/emails/emails/**", "packages/internal-pulsar/src/**", @@ -233,7 +233,6 @@ "apps/webapp/types/**", "packages/internal-platform/src/**", "packages/common-schemas/src/**", - "packages/internal-integrations/src/**", "packages/trigger-providers/src/**", "packages/emails/src/**", "packages/emails/emails/**", diff --git a/package.json b/package.json index d79b897c66c..a82369bc1ee 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "private": true, "workspaces": [ "apps/*", - "packages/*" + "packages/*", + "integrations/*" ], "version": "0.0.1", "prisma": { diff --git a/packages/integration-catalog/package.json b/packages/integration-catalog/package.json index 5a3bb4ec1cf..abdaebe62ea 100644 --- a/packages/integration-catalog/package.json +++ b/packages/integration-catalog/package.json @@ -2,6 +2,7 @@ "name": "integration-catalog", "version": "0.1.16", "description": "Catalog of all the Trigger.dev integration packages", + "private": true, "devDependencies": { "@trigger.dev/tsconfig": "workspace:*", "@types/node": "16", diff --git a/packages/internal-cli/tsconfig.json b/packages/internal-cli/tsconfig.json index 560d6154240..debe702fdf5 100644 --- a/packages/internal-cli/tsconfig.json +++ b/packages/internal-cli/tsconfig.json @@ -8,8 +8,8 @@ "outDir": "lib", "moduleResolution": "node", "paths": { - "@trigger.dev/providers": ["../trigger-providers/src/index"], - "@trigger.dev/providers/*": ["../trigger-providers/src/*"] + "integration-catalog": ["../integration-catalog/src/index"], + "integration-catalog/*": ["../integration-catalog/src/*"] } }, "exclude": ["node_modules", "**/*.test.*"] From 064e27f05d29576d99b25a3ea29b384e108cf55b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 16:31:02 +0000 Subject: [PATCH 34/71] Update all the docs for the new packages --- DEVELOPMENT.md | 10 ++---- apps/docs/examples/github.mdx | 6 ++-- apps/docs/examples/resend.mdx | 3 +- apps/docs/examples/shopify.mdx | 12 +++---- apps/docs/examples/slack.mdx | 3 +- apps/docs/functions/delays.mdx | 6 ++-- apps/docs/getting-started.mdx | 34 +++++++++++-------- apps/docs/guides/event-driven.mdx | 2 +- apps/docs/guides/zod.mdx | 2 +- .../apis/github/events/issue-comments.mdx | 2 +- .../apis/github/events/issues.mdx | 2 +- .../apis/github/events/new-star.mdx | 4 +-- .../github/events/pull-request-comments.mdx | 2 +- .../github/events/pull-request-reviews.mdx | 2 +- .../apis/github/events/pull-requests.mdx | 2 +- .../integrations/apis/github/events/push.mdx | 2 +- .../apis/resend/actions/send-email.mdx | 3 +- .../apis/slack/actions/post-message.mdx | 5 +-- apps/docs/triggers/custom-events.mdx | 2 +- apps/docs/triggers/webhooks.mdx | 5 +-- apps/webapp/remix.config.js | 1 - config-packages/tsconfig/examples.json | 6 ---- package.json | 2 +- 23 files changed, 57 insertions(+), 61 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8f667b8fc80..fdf3ca4d4cb 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -188,16 +188,10 @@ pnpm run dev --filter=webapp pnpm run dev --filter=wss ``` -4. Run the @trigger.dev/sdk:dev +4. Build all the @trigger.dev/\* packages ```bash -pnpm run dev --filter=@trigger.dev/sdk -``` - -5. Run the @trigger.dev integrations - -```bash -pnpm run dev --filter=@trigger.dev/integrations +pnpm run dev --filter="@trigger.dev/*" ``` ## Running the smoke test diff --git a/apps/docs/examples/github.mdx b/apps/docs/examples/github.mdx index 12c61a0d95a..f7db5254f3a 100644 --- a/apps/docs/examples/github.mdx +++ b/apps/docs/examples/github.mdx @@ -11,8 +11,9 @@ You can explore all the supported GitHub functions [here](/integrations/apis/git Integrations required: GitHub, Slack ```ts -import { github, slack } from "@trigger.dev/integrations"; import { Trigger } from "@trigger.dev/sdk"; +import * as github from "@trigger.dev/github"; +import * as slack from "@trigger.dev/slack"; new Trigger({ id: "new-github-star-to-slack", @@ -35,8 +36,9 @@ new Trigger({ Integrations required: GitHub, Slack ```ts -import { github, slack } from "@trigger.dev/integrations"; import { Trigger } from "@trigger.dev/sdk"; +import * as github from "@trigger.dev/github"; +import * as slack from "@trigger.dev/slack"; new Trigger({ id: "my-workflow-1", diff --git a/apps/docs/examples/resend.mdx b/apps/docs/examples/resend.mdx index 36ce42ac255..07e03c541cb 100644 --- a/apps/docs/examples/resend.mdx +++ b/apps/docs/examples/resend.mdx @@ -17,8 +17,9 @@ This workflow will get triggered and a Slack notification and welcom email will Integrations required: Resend, Slack ```ts -import { resend, slack } from "@trigger.dev/integrations"; import { customEvent, Trigger, sendEvent } from "@trigger.dev/sdk"; +import * as resend from "@trigger.dev/resend"; +import * as slack from "@trigger.dev/slack"; import React from "react"; import { z } from "zod"; import { getUser } from "../db"; diff --git a/apps/docs/examples/shopify.mdx b/apps/docs/examples/shopify.mdx index cebd371e761..df6c60743e7 100644 --- a/apps/docs/examples/shopify.mdx +++ b/apps/docs/examples/shopify.mdx @@ -11,13 +11,12 @@ Integrations required: Shopify ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; import { z } from "zod"; -import { shopify } from "@trigger.dev/integrations"; +import * as shopify from "@trigger.dev/shopify"; new Trigger({ id: "shopify-products", name: "Shopify products", apiKey: "", - logLevel: "debug", on: customEvent({ name: "shopify.products", schema: z.object({}), @@ -76,13 +75,12 @@ Integrations required: Shopify ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; import { z } from "zod"; -import { shopify } from "@trigger.dev/integrations"; +import * as shopify from "@trigger.dev/shopify"; new Trigger({ id: "shopify-update-product", name: "Shopify update product", apiKey: "", - logLevel: "debug", on: customEvent({ name: "product.update", schema: z.object({ @@ -107,13 +105,12 @@ Integrations required: Shopify ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; import { z } from "zod"; -import { shopify } from "@trigger.dev/integrations"; +import * as shopify from "@trigger.dev/shopify"; new Trigger({ id: "shopify-add-product-to-collection", name: "Shopify add product to collection", apiKey: "", - logLevel: "debug", on: customEvent({ name: "product.new", schema: z.object({ @@ -141,7 +138,7 @@ Integrations required: Shopify ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; import { z } from "zod"; -import { shopify } from "@trigger.dev/integrations"; +import * as shopify from "@trigger.dev/shopify"; const sizes = ["Small", "Medium", "Large"]; const colors = [ @@ -170,7 +167,6 @@ new Trigger({ id: "shopify-product-variants", name: "Shopify product variants", apiKey: "", - logLevel: "debug", on: customEvent({ name: "shopify.product-variants", schema: z.object({}), diff --git a/apps/docs/examples/slack.mdx b/apps/docs/examples/slack.mdx index ac7f9628df8..c464171c95b 100644 --- a/apps/docs/examples/slack.mdx +++ b/apps/docs/examples/slack.mdx @@ -10,7 +10,8 @@ Integrations required: Slack, GitHub ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { github, slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; +import * as github from "@trigger.dev/github"; import { z } from "zod"; new Trigger({ diff --git a/apps/docs/functions/delays.mdx b/apps/docs/functions/delays.mdx index d818c37d809..f1d7ae8d98b 100644 --- a/apps/docs/functions/delays.mdx +++ b/apps/docs/functions/delays.mdx @@ -20,7 +20,8 @@ This example posts a message to Slack after a 30 second delay: ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { github, slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; +import * as github from "@trigger.dev/github"; import { z } from "zod"; new Trigger({ @@ -62,7 +63,8 @@ This example posts a message to Slack 1 hour after the workflow triggers: ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { github, slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; +import * as github from "@trigger.dev/github"; import { z } from "zod"; new Trigger({ diff --git a/apps/docs/getting-started.mdx b/apps/docs/getting-started.mdx index 83f9bc61c5c..80f6d1b57de 100644 --- a/apps/docs/getting-started.mdx +++ b/apps/docs/getting-started.mdx @@ -22,21 +22,21 @@ In your existing project install the packages: ```bash -npm install @trigger.dev/sdk @trigger.dev/integrations zod +npm install @trigger.dev/sdk @trigger.dev/slack zod ``` ```bash -pnpm install @trigger.dev/sdk @trigger.dev/integrations zod +pnpm install @trigger.dev/sdk @trigger.dev/slack zod ``` ```bash -yarn add @trigger.dev/sdk @trigger.dev/integrations zod +yarn add @trigger.dev/sdk @trigger.dev/slack zod ``` @@ -45,7 +45,7 @@ yarn add @trigger.dev/sdk @trigger.dev/integrations zod What do the packages do? - `@trigger.dev/sdk` is required to use Trigger.dev, it allows you to run workflows. -- `@trigger.dev/integrations` allows you to easily subscribe to webhooks and use API calls from popular services. +- `@trigger.dev/slack` is the official Slack integration, which allows you to interact with the Slack API. - `zod` is used to define schemas (the expected shape of an object). ## 2. Sign in to your Trigger.dev dashboard @@ -63,14 +63,13 @@ Workflows are triggered by events. In this example, we have defined a custom eve ```ts first-workflow.ts import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { slack } from "@trigger.dev/integrations"; +import { postMessage } from "@trigger.dev/slack"; import { z } from "zod"; -const postMessage = new Trigger({ +new Trigger({ id: "new-user", name: "New user slack message", apiKey: "", - logLevel: "info", on: customEvent({ name: "user.created", schema: z.object({ @@ -83,7 +82,7 @@ const postMessage = new Trigger({ await ctx.logger.info("This log will appear on the Trigger.dev run page"); //send a message to the #new-users Slack channel with user details - const response = await slack.postMessage("send-to-slack", { + const response = await postMessage("send-to-slack", { channel: "new-users", text: `New user: ${event.name} (${event.email}) signed up. ${ event.paidPlan ? "They are paying" : "They are on the free plan" @@ -92,10 +91,7 @@ const postMessage = new Trigger({ return response.message; }, -}); - -//this workflow will now connect and start listening for events -postMessage.listen(); +}).listen(); ``` You'll notice that when we subscribe to the custom event we have to say the name of the event and provide a schema. Schemas are created using Zod. In this case events must send an object that has `name`, `email`, and `paidPlan`. @@ -168,9 +164,9 @@ import { sendEvent } from "@trigger.dev/sdk"; ...your other code */ -await sendEvent({ - apiKey: "", - event: { +await sendEvent(uuidv4(), { + name: "user.created" + payload: { name: "Eleven", email: "jane@hawksmoorhigh.edu", paidPlan: true, @@ -178,8 +174,16 @@ await sendEvent({ }); ``` + + If you are using `sendEvent` outside of a Trigger, make sure and set your API + key to the `TRIGGER_API_KEY` environment variable. See the + [sendEvent](/functions/send-event) docs for more info. + + When you run your server and this code executes, it will trigger the workflow. You will see this in the run list on your workflow page. +You can also use our API to send events from other programming languages. See the [sendEvent](/api/events/sendEvent) API reference for more info. + ## Next steps ```typescript Notify Slack on New GitHub Star -import { slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; new Trigger({ id: "new-star", diff --git a/apps/docs/integrations/apis/github/events/pull-request-comments.mdx b/apps/docs/integrations/apis/github/events/pull-request-comments.mdx index c76a961e2f1..57407d0a73d 100644 --- a/apps/docs/integrations/apis/github/events/pull-request-comments.mdx +++ b/apps/docs/integrations/apis/github/events/pull-request-comments.mdx @@ -13,7 +13,7 @@ description: "Trigger a workflow whenever an pull_request_review_comment action ## Usage ```ts -import { github } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; new Trigger({ id: "demo", diff --git a/apps/docs/integrations/apis/github/events/pull-request-reviews.mdx b/apps/docs/integrations/apis/github/events/pull-request-reviews.mdx index 4f370b9e785..e29ac64f96e 100644 --- a/apps/docs/integrations/apis/github/events/pull-request-reviews.mdx +++ b/apps/docs/integrations/apis/github/events/pull-request-reviews.mdx @@ -7,7 +7,7 @@ description: "Trigger a workflow whenever an pull_request_review action is perfo ## Usage ```ts -import { github } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; new Trigger({ id: "demo", diff --git a/apps/docs/integrations/apis/github/events/pull-requests.mdx b/apps/docs/integrations/apis/github/events/pull-requests.mdx index be89fe5a7cb..b2e6dc69f8a 100644 --- a/apps/docs/integrations/apis/github/events/pull-requests.mdx +++ b/apps/docs/integrations/apis/github/events/pull-requests.mdx @@ -7,7 +7,7 @@ description: "Trigger a workflow whenever an pull_request action is performed in ## Usage ```ts -import { github } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; new Trigger({ id: "demo", diff --git a/apps/docs/integrations/apis/github/events/push.mdx b/apps/docs/integrations/apis/github/events/push.mdx index b41b808b4bf..720f7cd478b 100644 --- a/apps/docs/integrations/apis/github/events/push.mdx +++ b/apps/docs/integrations/apis/github/events/push.mdx @@ -7,7 +7,7 @@ description: "Trigger a workflow whenever a push action is performed in a GitHub ## Usage ```ts -import { github } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; new Trigger({ id: "demo", diff --git a/apps/docs/integrations/apis/resend/actions/send-email.mdx b/apps/docs/integrations/apis/resend/actions/send-email.mdx index 1ab79e7c8b8..d2772491bf8 100644 --- a/apps/docs/integrations/apis/resend/actions/send-email.mdx +++ b/apps/docs/integrations/apis/resend/actions/send-email.mdx @@ -85,7 +85,8 @@ Send email using Resend.com, which supports sending emails using React, HTML, an ```typescript criticalIssue.tsx -import { github, resend } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; +import * as resend from "@trigger.dev/resend"; import { CriticalIssueEmail } from "./emails"; new Trigger({ diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx index a7f446afba5..b0dd9624093 100644 --- a/apps/docs/integrations/apis/slack/actions/post-message.mdx +++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx @@ -82,7 +82,8 @@ Publish Slack messages to a public or private channel in your Slack Workspace as ```typescript Notify Slack on New GitHub Star -import { slack, github } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; +import * as slack from "@trigger.dev/slack"; new Trigger({ id: "new-star", @@ -133,7 +134,7 @@ Please note, that to use `jsx-slack` you will need to add the `jsxImportSource` ```typescript workflow.tsx /** @jsxImportSource jsx-slack */ -import { slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; import JSXSlack, { Actions, Blocks, diff --git a/apps/docs/triggers/custom-events.mdx b/apps/docs/triggers/custom-events.mdx index 0d226be8e27..3353cd6d314 100644 --- a/apps/docs/triggers/custom-events.mdx +++ b/apps/docs/triggers/custom-events.mdx @@ -61,7 +61,7 @@ Please see the [Event filters guide](/guides/event-filters) for more information ```ts import { Trigger, customEvent } from "@trigger.dev/sdk"; -import { slack } from "@trigger.dev/integrations"; +import * as slack from "@trigger.dev/slack"; import { z } from "zod"; //this workflow will run when a "user.created" event is sent diff --git a/apps/docs/triggers/webhooks.mdx b/apps/docs/triggers/webhooks.mdx index e30e860f03a..e26caa53c03 100644 --- a/apps/docs/triggers/webhooks.mdx +++ b/apps/docs/triggers/webhooks.mdx @@ -32,7 +32,7 @@ Please [join our discord community](https://discord.gg/kA47vcd8P6) and let us kn We've documented all the support webhooks for each integration in the sidebar, for example the GitHub [newStarEvent](/integrations/apis/github/events/new-star) webhook: ```ts -import { github } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; new Trigger({ id: "demo", @@ -83,7 +83,8 @@ Copy the URL and secret, and register the webhook with the provider (in this cas ```ts Github import { Trigger } from "@trigger.dev/sdk"; -import { github, slack } from "@trigger.dev/integrations"; +import * as github from "@trigger.dev/github"; +import * as slack from "@trigger.dev/slack"; new Trigger({ id: "escalate-critical-issues", diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index 8bd6bede068..d6c4bdf3c31 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -19,7 +19,6 @@ module.exports = { "@trigger.dev/shopify", "@trigger.dev/common-schemas", "@trigger.dev/sdk", - "@trigger.dev/integrations", "@trigger.dev/integration-sdk", "emails", "internal-pulsar", diff --git a/config-packages/tsconfig/examples.json b/config-packages/tsconfig/examples.json index bcd63ce49a7..6426fe4eaa9 100644 --- a/config-packages/tsconfig/examples.json +++ b/config-packages/tsconfig/examples.json @@ -24,12 +24,6 @@ "@trigger.dev/sdk/*": [ "../../packages/trigger-sdk/src/*" ], - "@trigger.dev/integrations": [ - "../../packages/trigger-integrations/src/index" - ], - "@trigger.dev/integrations/*": [ - "../../packages/trigger-integrations/src/*" - ], "@trigger.dev/github": [ "../../integrations/github/src/index" ], diff --git a/package.json b/package.json index a82369bc1ee..adbeb2a25fe 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "env:pull": "turbo run env:pull", "changeset:add": "changeset", "changeset:version": "changeset version", - "changeset:release": "pnpm run build --filter @trigger.dev/integrations --filter @trigger.dev/sdk && changeset publish" + "changeset:release": "pnpm run build --filter \"@trigger.dev/*\" && changeset publish" }, "devDependencies": { "@manypkg/cli": "^0.19.2", From fd54c86240f6444d6fa2aa8d3288be2760a4114b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 30 Jan 2023 16:55:44 +0000 Subject: [PATCH 35/71] Updated s-webapp watch paths --- flightcontrol.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flightcontrol.json b/flightcontrol.json index 052956e39c0..e70efd28180 100644 --- a/flightcontrol.json +++ b/flightcontrol.json @@ -233,7 +233,8 @@ "apps/webapp/types/**", "packages/internal-platform/src/**", "packages/common-schemas/src/**", - "packages/trigger-providers/src/**", + "integrations/*/src/**", + "packages/integration-catalog/src/**", "packages/emails/src/**", "packages/emails/emails/**", "packages/internal-pulsar/src/**", From 3496053e1136400a05308cf5d94bd5d898ee8d40 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 30 Jan 2023 17:23:12 +0000 Subject: [PATCH 36/71] Updated the CLI to point at the internal integrations metadata --- packages/internal-cli/src/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/internal-cli/src/index.ts b/packages/internal-cli/src/index.ts index 9f750824d0f..07ca086a749 100644 --- a/packages/internal-cli/src/index.ts +++ b/packages/internal-cli/src/index.ts @@ -65,7 +65,7 @@ program } const authProviders = result.data; const pizzly_host = options.pizzlyhost ?? "http://localhost:3004"; - const providers = getProviders(true); + const providers = getIntegrations(true); console.log(`Using pizzly host: ${pizzly_host}`); @@ -84,15 +84,15 @@ program return Promise.resolve(); } - const provider = providers.find((p) => p.slug === service); + const provider = providers.find((p) => p.metadata.slug === service); if (!provider) { console.log(`No provider found for ${service}`); console.log("Skipping…"); return Promise.resolve(); } - if (provider.authentication.type !== "oauth") { + if (provider.metadata.authentication.type !== "oauth") { console.log( - `The provider ${service} is the wrong type ${provider?.authentication.type}. Must be oauth` + `The provider ${service} is the wrong type ${provider?.metadata.authentication.type}. Must be oauth` ); console.log("Skipping…"); return Promise.resolve(); @@ -132,12 +132,12 @@ program service, environmentClientId, client_secret, - provider.authentication.scopes, + provider.metadata.authentication.scopes, options.pizzlysecretkey ); console.log( `Updated config for ${service} with scopes`, - provider.authentication.scopes + provider.metadata.authentication.scopes ); } else { const response = await createConfig( @@ -145,12 +145,12 @@ program service, environmentClientId, client_secret, - provider.authentication.scopes, + provider.metadata.authentication.scopes, options.pizzlysecretkey ); console.log( `Created config for ${service} with scopes`, - provider.authentication.scopes + provider.metadata.authentication.scopes ); } } catch (error) { From 7b29422a57ae5345d0a4ada009c38225a0d8c5d4 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 09:46:34 +0000 Subject: [PATCH 37/71] Initial setup on new WhatsApp integration --- apps/webapp/package.json | 3 +- apps/webapp/tsconfig.json | 2 + integrations/whatsapp/README.md | 3 + integrations/whatsapp/package.json | 38 +++++++++ integrations/whatsapp/src/events.ts | 27 ++++++ integrations/whatsapp/src/index.ts | 3 + integrations/whatsapp/src/internal.ts | 27 ++++++ .../whatsapp/src/internal/webhooks.ts | 40 +++++++++ integrations/whatsapp/src/schemas/index.ts | 9 ++ .../whatsapp/src/schemas/messageEvents.ts | 3 + integrations/whatsapp/src/schemas/shared.ts | 1 + integrations/whatsapp/tsconfig.json | 20 +++++ integrations/whatsapp/tsup.config.ts | 23 +++++ packages/integration-catalog/package.json | 3 +- packages/integration-catalog/src/index.ts | 2 + packages/integration-catalog/tsconfig.json | 4 +- pnpm-lock.yaml | 83 ++++++++++--------- 17 files changed, 251 insertions(+), 40 deletions(-) create mode 100644 integrations/whatsapp/README.md create mode 100644 integrations/whatsapp/package.json create mode 100644 integrations/whatsapp/src/events.ts create mode 100644 integrations/whatsapp/src/index.ts create mode 100644 integrations/whatsapp/src/internal.ts create mode 100644 integrations/whatsapp/src/internal/webhooks.ts create mode 100644 integrations/whatsapp/src/schemas/index.ts create mode 100644 integrations/whatsapp/src/schemas/messageEvents.ts create mode 100644 integrations/whatsapp/src/schemas/shared.ts create mode 100644 integrations/whatsapp/tsconfig.json create mode 100644 integrations/whatsapp/tsup.config.ts diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 824f1e39a71..964eb79950c 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -68,6 +68,7 @@ "@trigger.dev/slack": "workspace:*", "@trigger.dev/resend": "workspace:*", "@trigger.dev/shopify": "workspace:*", + "@trigger.dev/whatsapp": "workspace:*", "@typeform/embed-react": "^2.14.1", "@uiw/react-codemirror": "^4.13.2", "bcryptjs": "^2.4.3", @@ -190,4 +191,4 @@ "engines": { "node": ">=16.0.0" } -} \ No newline at end of file +} diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index fdec63efed0..ad1b2a1cb7d 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -44,6 +44,8 @@ "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"], + "@trigger.dev/whatsapp": ["../../integrations/whatsapp/src/index"], + "@trigger.dev/whatsapp/*": ["../../integrations/whatsapp/src/*"], "integration-catalog": ["../../packages/integration-catalog/src/index"], "integration-catalog/*": ["../../packages/integration-catalog/src/*"] }, diff --git a/integrations/whatsapp/README.md b/integrations/whatsapp/README.md new file mode 100644 index 00000000000..ae6e2cc584e --- /dev/null +++ b/integrations/whatsapp/README.md @@ -0,0 +1,3 @@ +# Trigger.dev WhatsApp Business integration + +View more documentation [here](https://docs.trigger.dev) diff --git a/integrations/whatsapp/package.json b/integrations/whatsapp/package.json new file mode 100644 index 00000000000..34a6fc01164 --- /dev/null +++ b/integrations/whatsapp/package.json @@ -0,0 +1,38 @@ +{ + "name": "@trigger.dev/whatsapp", + "version": "0.1.16", + "description": "The official WhatsApp Business integration for Trigger.dev", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/tsconfig": "workspace:*", + "@trigger.dev/integration-sdk": "workspace:*", + "@types/node": "16", + "rimraf": "^3.0.2", + "tsup": "^6.5.0" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:*" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup" + }, + "dependencies": { + "zod": "^3.20.2" + } +} diff --git a/integrations/whatsapp/src/events.ts b/integrations/whatsapp/src/events.ts new file mode 100644 index 00000000000..d9d3cb79ef6 --- /dev/null +++ b/integrations/whatsapp/src/events.ts @@ -0,0 +1,27 @@ +import type { TriggerEvent } from "@trigger.dev/sdk"; +import * as schemas from "./schemas"; + +export function messageEvent(params: { + repo: string; +}): TriggerEvent { + return { + metadata: { + type: "WEBHOOK", + service: "whatsapp", + name: "message", + filter: { + service: ["whatsapp"], + payload: {}, + event: ["message"], + }, + source: { + verifyPayload: { + enabled: true, + }, + event: "message", + }, + manualRegistration: true, + }, + schema: schemas.messageEvents.messageEventSchema, + }; +} diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts new file mode 100644 index 00000000000..324692d48bf --- /dev/null +++ b/integrations/whatsapp/src/index.ts @@ -0,0 +1,3 @@ +import * as events from "./events"; + +export { events }; diff --git a/integrations/whatsapp/src/internal.ts b/integrations/whatsapp/src/internal.ts new file mode 100644 index 00000000000..c1736765826 --- /dev/null +++ b/integrations/whatsapp/src/internal.ts @@ -0,0 +1,27 @@ +import type { + IntegrationMetadata, + InternalIntegration, +} from "@trigger.dev/integration-sdk"; +import { WhatsAppWebhookIntegration } from "./internal/webhooks"; + +const webhooks = new WhatsAppWebhookIntegration(); + +const metadata: IntegrationMetadata = { + name: "WhatsApp Business Account", + slug: "whatsapp", + icon: "/integrations/whatsapp.png", + enabledFor: "admins", + authentication: { + type: "api_key", + header_name: "Authorization", + header_type: "access_token", + documentation: `You need to generate a "permanent access token"\nFollow the steps in [this documentation](https://developers.facebook.com/docs/whatsapp/business-management-api/get-started#1--acquire-an-access-token-using-a-system-user-or-facebook-login).`, + }, +}; + +export const internalIntegration: InternalIntegration = { + metadata, + webhooks, +}; + +export * as schemas from "./schemas"; diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts new file mode 100644 index 00000000000..5946c6db9a3 --- /dev/null +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { getAccessToken } from "@trigger.dev/integration-sdk"; +import type { + DisplayProperty, + HandleWebhookOptions, + WebhookConfig, + WebhookIntegration, +} from "@trigger.dev/integration-sdk"; +import { WebhookSourceSchema } from "../schemas"; + +export class WhatsAppWebhookIntegration implements WebhookIntegration { + keyForSource(source: unknown): string { + const whatsAppSource = parseWebhookSource(source); + //todo add some identifier in here for the WhatsApp account/number + return `messages.${whatsAppSource.events.join(".")}`; + } + + registerWebhook(config: WebhookConfig, source: unknown) { + return Promise.reject("Not implemented"); + } + + handleWebhookRequest(options: HandleWebhookOptions) { + console.log("Handling WhatsApp webhook request", options.request.body); + + return { + status: "ok" as const, + data: { id: "", payload: options.request.body, event: "", context: {} }, + }; + } + + displayProperties(source: unknown) { + return { title: "WhatsApp", properties: [] }; + } +} + +export const webhooks = new WhatsAppWebhookIntegration(); + +function parseWebhookSource(source: unknown) { + return WebhookSourceSchema.parse(source); +} diff --git a/integrations/whatsapp/src/schemas/index.ts b/integrations/whatsapp/src/schemas/index.ts new file mode 100644 index 00000000000..7b341211bd6 --- /dev/null +++ b/integrations/whatsapp/src/schemas/index.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; +import * as messageEvents from "./messageEvents"; + +export const WebhookSourceSchema = z.object({ + scopes: z.array(z.string()), + events: z.array(z.string()), +}); + +export { messageEvents }; diff --git a/integrations/whatsapp/src/schemas/messageEvents.ts b/integrations/whatsapp/src/schemas/messageEvents.ts new file mode 100644 index 00000000000..fdc5a9af737 --- /dev/null +++ b/integrations/whatsapp/src/schemas/messageEvents.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const messageEventSchema = z.any(); diff --git a/integrations/whatsapp/src/schemas/shared.ts b/integrations/whatsapp/src/schemas/shared.ts new file mode 100644 index 00000000000..9d50a9238cf --- /dev/null +++ b/integrations/whatsapp/src/schemas/shared.ts @@ -0,0 +1 @@ +import { z } from "zod"; diff --git a/integrations/whatsapp/tsconfig.json b/integrations/whatsapp/tsconfig.json new file mode 100644 index 00000000000..e159cec9c07 --- /dev/null +++ b/integrations/whatsapp/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@trigger.dev/tsconfig/node16.json", + "include": ["./src/**/*.ts", "tsup.config.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-sdk/*": [ + "../../packages/integration-sdk/src/*" + ], + "@trigger.dev/integration-sdk": [ + "../../packages/integration-sdk/src/index" + ] + } + }, + "exclude": ["node_modules"] +} diff --git a/integrations/whatsapp/tsup.config.ts b/integrations/whatsapp/tsup.config.ts new file mode 100644 index 00000000000..a71244deb1a --- /dev/null +++ b/integrations/whatsapp/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + treeshake: { + preset: "smallest", + }, + external: ["http", "https", "util", "events", "tty", "os", "timers"], + esbuildPlugins: [], + noExternal: ["@trigger.dev/common-schemas"], + }, +]); diff --git a/packages/integration-catalog/package.json b/packages/integration-catalog/package.json index abdaebe62ea..649ec5072f6 100644 --- a/packages/integration-catalog/package.json +++ b/packages/integration-catalog/package.json @@ -10,8 +10,9 @@ "@trigger.dev/github": "workspace:*", "@trigger.dev/shopify": "workspace:*", "@trigger.dev/resend": "workspace:*", + "@trigger.dev/whatsapp": "workspace:*", "@trigger.dev/integration-sdk": "workspace:*" }, "scripts": {}, "dependencies": {} -} \ No newline at end of file +} diff --git a/packages/integration-catalog/src/index.ts b/packages/integration-catalog/src/index.ts index 90a0ecb874d..20af2d93273 100644 --- a/packages/integration-catalog/src/index.ts +++ b/packages/integration-catalog/src/index.ts @@ -2,6 +2,7 @@ import { internalIntegration as slack } from "@trigger.dev/slack/internal"; import { internalIntegration as github } from "@trigger.dev/github/internal"; import { internalIntegration as resend } from "@trigger.dev/resend/internal"; import { internalIntegration as shopify } from "@trigger.dev/shopify/internal"; +import { internalIntegration as whatsapp } from "@trigger.dev/whatsapp/internal"; import type { InternalIntegration } from "@trigger.dev/integration-sdk"; @@ -37,6 +38,7 @@ const catalog = { resend, shopify, slack, + whatsapp, }, }; diff --git a/packages/integration-catalog/tsconfig.json b/packages/integration-catalog/tsconfig.json index eb4f69841e9..b7bba83d6b3 100644 --- a/packages/integration-catalog/tsconfig.json +++ b/packages/integration-catalog/tsconfig.json @@ -19,7 +19,9 @@ "@trigger.dev/resend": ["../../integrations/resend/src/index"], "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], - "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"] + "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"], + "@trigger.dev/whatsapp": ["../../integrations/whatsapp/src/index"], + "@trigger.dev/whatsapp/*": ["../../integrations/whatsapp/src/*"] } }, "exclude": ["node_modules"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6792d02c02e..88f804269d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: '@aws-sdk/client-s3': 3.245.0 '@aws-sdk/s3-request-presigner': 3.245.0 '@cfworker/json-schema': 1.12.5 - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 @@ -217,7 +217,7 @@ importers: '@trigger.dev/shopify': link:../../integrations/shopify '@trigger.dev/slack': link:../../integrations/slack '@typeform/embed-react': 2.14.1_react@18.2.0 - '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle + '@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne bcryptjs: 2.4.3 classnames: 2.3.2 clsx: 1.2.1 @@ -596,7 +596,7 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@react-email/render': 0.0.3 + '@react-email/render': 0.0.3_react@18.2.0 debug: 4.3.4 zod: 3.20.2 devDependencies: @@ -667,6 +667,25 @@ importers: rimraf: 3.0.2 tsup: 6.5.0 + integrations/whatsapp: + specifiers: + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/node': '16' + rimraf: ^3.0.2 + tsup: ^6.5.0 + zod: ^3.20.2 + dependencies: + zod: 3.20.2 + devDependencies: + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + rimraf: 3.0.2 + tsup: 6.5.0 + packages/common-schemas: specifiers: '@trigger.dev/tsconfig': workspace:* @@ -731,6 +750,7 @@ importers: '@trigger.dev/shopify': workspace:* '@trigger.dev/slack': workspace:* '@trigger.dev/tsconfig': workspace:* + '@trigger.dev/whatsapp': workspace:* '@types/node': '16' devDependencies: '@trigger.dev/github': link:../../integrations/github @@ -739,6 +759,7 @@ importers: '@trigger.dev/shopify': link:../../integrations/shopify '@trigger.dev/slack': link:../../integrations/slack '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@trigger.dev/whatsapp': link:../../integrations/whatsapp '@types/node': 16.18.11 packages/integration-sdk: @@ -3523,12 +3544,13 @@ packages: prettier: 2.8.2 dev: false - /@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu: + /@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde: resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==} peerDependencies: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.3.2 '@codemirror/state': 6.2.0 @@ -3548,7 +3570,7 @@ packages: /@codemirror/lang-javascript/6.1.2: resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==} dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/state': 6.2.0 @@ -4749,16 +4771,6 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@react-email/render/0.0.3: - resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} - engines: {node: '>=18.0.0'} - dependencies: - pretty: 2.0.0 - react-dom: 18.2.0 - transitivePeerDependencies: - - react - dev: false - /@react-email/render/0.0.3_react@18.2.0: resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} engines: {node: '>=18.0.0'} @@ -4874,7 +4886,7 @@ packages: eslint: 8.31.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy - eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny + eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq eslint-plugin-jest: 26.9.0_ohsifnwenhmxgcp7mend4dnv74 eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0 @@ -6011,17 +6023,18 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: + /@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 @@ -6030,11 +6043,14 @@ packages: '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: + /@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: + '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: @@ -6043,13 +6059,14 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom - codemirror: 6.0.1 + '@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e + codemirror: 6.0.1_@lezer+common@1.0.2 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' + - '@codemirror/lint' - '@codemirror/search' dev: false @@ -7360,16 +7377,18 @@ packages: engines: {node: '>=0.10.0'} dev: false - /codemirror/6.0.1: + /codemirror/6.0.1_@lezer+common@1.0.2: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 + transitivePeerDependencies: + - '@lezer/common' dev: false /collection-visit/1.0.0: @@ -8706,7 +8725,7 @@ packages: debug: 4.3.4 enhanced-resolve: 5.12.0 eslint: 8.31.0 - eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny + eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq get-tsconfig: 4.3.0 globby: 13.1.3 is-core-module: 2.11.0 @@ -8716,7 +8735,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq: + /eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -8741,7 +8760,6 @@ packages: debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy transitivePeerDependencies: - supports-color dev: true @@ -8766,7 +8784,7 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-import/2.27.4_2ac3tknkazjoq5fxmuugu665ny: + /eslint-plugin-import/2.27.4_qdjeohovcytra7xto5vgmxssaq: resolution: {integrity: sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==} engines: {node: '>=4'} peerDependencies: @@ -8784,7 +8802,7 @@ packages: doctrine: 2.1.0 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq + eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -13887,15 +13905,6 @@ packages: shallow-equal: 1.2.1 dev: false - /react-dom/18.2.0: - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - scheduler: 0.23.0 - dev: false - /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: From a20a26e748fa0e5bf856782f4a5d8ba49cf89bf2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 09:53:24 +0000 Subject: [PATCH 38/71] Updated settings for WhatsApp to match other integration packages --- apps/webapp/tsconfig.json | 2 ++ integrations/whatsapp/package.json | 9 ++++++++- integrations/whatsapp/tsup.config.ts | 16 ++++++++++++++++ packages/integration-catalog/package.json | 3 ++- packages/integration-catalog/src/index.ts | 2 ++ packages/integration-catalog/tsconfig.json | 4 +++- 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index fdec63efed0..ad1b2a1cb7d 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -44,6 +44,8 @@ "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"], + "@trigger.dev/whatsapp": ["../../integrations/whatsapp/src/index"], + "@trigger.dev/whatsapp/*": ["../../integrations/whatsapp/src/*"], "integration-catalog": ["../../packages/integration-catalog/src/index"], "integration-catalog/*": ["../../packages/integration-catalog/src/*"] }, diff --git a/integrations/whatsapp/package.json b/integrations/whatsapp/package.json index 34a6fc01164..d55816a6c84 100644 --- a/integrations/whatsapp/package.json +++ b/integrations/whatsapp/package.json @@ -8,13 +8,20 @@ "require": "./dist/index.js", "types": "./dist/index.d.ts" }, + "./internal": { + "import": "./dist/internal.js", + "require": "./dist/internal.js", + "types": "./dist/internal.d.ts" + }, "./package.json": "./package.json" }, "publishConfig": { "access": "public" }, "files": [ - "dist" + "dist/index.js", + "dist/index.d.ts", + "dist/index.js.map" ], "devDependencies": { "@trigger.dev/sdk": "workspace:*", diff --git a/integrations/whatsapp/tsup.config.ts b/integrations/whatsapp/tsup.config.ts index a71244deb1a..a74fd911579 100644 --- a/integrations/whatsapp/tsup.config.ts +++ b/integrations/whatsapp/tsup.config.ts @@ -16,8 +16,24 @@ export default defineConfig([ treeshake: { preset: "smallest", }, + esbuildPlugins: [], + noExternal: ["@trigger.dev/common-schemas"], external: ["http", "https", "util", "events", "tty", "os", "timers"], + }, + { + name: "internal", + entry: ["./src/internal.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, esbuildPlugins: [], noExternal: ["@trigger.dev/common-schemas"], + external: ["http", "https", "util", "events", "tty", "os", "timers"], }, ]); diff --git a/packages/integration-catalog/package.json b/packages/integration-catalog/package.json index abdaebe62ea..649ec5072f6 100644 --- a/packages/integration-catalog/package.json +++ b/packages/integration-catalog/package.json @@ -10,8 +10,9 @@ "@trigger.dev/github": "workspace:*", "@trigger.dev/shopify": "workspace:*", "@trigger.dev/resend": "workspace:*", + "@trigger.dev/whatsapp": "workspace:*", "@trigger.dev/integration-sdk": "workspace:*" }, "scripts": {}, "dependencies": {} -} \ No newline at end of file +} diff --git a/packages/integration-catalog/src/index.ts b/packages/integration-catalog/src/index.ts index 90a0ecb874d..20af2d93273 100644 --- a/packages/integration-catalog/src/index.ts +++ b/packages/integration-catalog/src/index.ts @@ -2,6 +2,7 @@ import { internalIntegration as slack } from "@trigger.dev/slack/internal"; import { internalIntegration as github } from "@trigger.dev/github/internal"; import { internalIntegration as resend } from "@trigger.dev/resend/internal"; import { internalIntegration as shopify } from "@trigger.dev/shopify/internal"; +import { internalIntegration as whatsapp } from "@trigger.dev/whatsapp/internal"; import type { InternalIntegration } from "@trigger.dev/integration-sdk"; @@ -37,6 +38,7 @@ const catalog = { resend, shopify, slack, + whatsapp, }, }; diff --git a/packages/integration-catalog/tsconfig.json b/packages/integration-catalog/tsconfig.json index eb4f69841e9..b7bba83d6b3 100644 --- a/packages/integration-catalog/tsconfig.json +++ b/packages/integration-catalog/tsconfig.json @@ -19,7 +19,9 @@ "@trigger.dev/resend": ["../../integrations/resend/src/index"], "@trigger.dev/resend/*": ["../../integrations/resend/src/*"], "@trigger.dev/shopify": ["../../integrations/shopify/src/index"], - "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"] + "@trigger.dev/shopify/*": ["../../integrations/shopify/src/*"], + "@trigger.dev/whatsapp": ["../../integrations/whatsapp/src/index"], + "@trigger.dev/whatsapp/*": ["../../integrations/whatsapp/src/*"] } }, "exclude": ["node_modules"] From a4a64affd4483aa9828809eef7244e9f2d0e56a9 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 10:18:21 +0000 Subject: [PATCH 39/71] Make the generic webhook input fields readonly so to get rid of React error --- .../$organizationSlug/workflows/$workflowSlug/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx index 377d5bce542..e533adcda24 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx @@ -107,7 +107,10 @@ export default function Page() { URL
    - + @@ -125,6 +128,7 @@ export default function Page() { Date: Tue, 31 Jan 2023 11:56:15 +0000 Subject: [PATCH 40/71] Installed @trigger.dev/whatsapp --- pnpm-lock.yaml | 85 ++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f0b03c5b8..c47d6c3fd67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,7 @@ importers: '@trigger.dev/shopify': workspace:* '@trigger.dev/slack': workspace:* '@trigger.dev/tailwind-config': workspace:* + '@trigger.dev/whatsapp': workspace:* '@typeform/embed-react': ^2.14.1 '@types/bcryptjs': ^2.4.2 '@types/compression': ^1.7.2 @@ -188,7 +189,7 @@ importers: '@aws-sdk/client-s3': 3.245.0 '@aws-sdk/s3-request-presigner': 3.245.0 '@cfworker/json-schema': 1.12.5 - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 @@ -216,8 +217,9 @@ importers: '@trigger.dev/resend': link:../../integrations/resend '@trigger.dev/shopify': link:../../integrations/shopify '@trigger.dev/slack': link:../../integrations/slack + '@trigger.dev/whatsapp': link:../../integrations/whatsapp '@typeform/embed-react': 2.14.1_react@18.2.0 - '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle + '@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne bcryptjs: 2.4.3 classnames: 2.3.2 clsx: 1.2.1 @@ -596,7 +598,7 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: - '@react-email/render': 0.0.3 + '@react-email/render': 0.0.3_react@18.2.0 debug: 4.3.4 zod: 3.20.2 devDependencies: @@ -667,6 +669,25 @@ importers: rimraf: 3.0.2 tsup: 6.5.0 + integrations/whatsapp: + specifiers: + '@trigger.dev/integration-sdk': workspace:* + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@types/node': '16' + rimraf: ^3.0.2 + tsup: ^6.5.0 + zod: ^3.20.2 + dependencies: + zod: 3.20.2 + devDependencies: + '@trigger.dev/integration-sdk': link:../../packages/integration-sdk + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + rimraf: 3.0.2 + tsup: 6.5.0 + packages/common-schemas: specifiers: '@trigger.dev/tsconfig': workspace:* @@ -731,6 +752,7 @@ importers: '@trigger.dev/shopify': workspace:* '@trigger.dev/slack': workspace:* '@trigger.dev/tsconfig': workspace:* + '@trigger.dev/whatsapp': workspace:* '@types/node': '16' devDependencies: '@trigger.dev/github': link:../../integrations/github @@ -739,6 +761,7 @@ importers: '@trigger.dev/shopify': link:../../integrations/shopify '@trigger.dev/slack': link:../../integrations/slack '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@trigger.dev/whatsapp': link:../../integrations/whatsapp '@types/node': 16.18.11 packages/integration-sdk: @@ -3523,12 +3546,13 @@ packages: prettier: 2.8.2 dev: false - /@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu: + /@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde: resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==} peerDependencies: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.3.2 '@codemirror/state': 6.2.0 @@ -3548,7 +3572,7 @@ packages: /@codemirror/lang-javascript/6.1.2: resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==} dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/state': 6.2.0 @@ -4746,16 +4770,6 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@react-email/render/0.0.3: - resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} - engines: {node: '>=18.0.0'} - dependencies: - pretty: 2.0.0 - react-dom: 18.2.0 - transitivePeerDependencies: - - react - dev: false - /@react-email/render/0.0.3_react@18.2.0: resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==} engines: {node: '>=18.0.0'} @@ -4871,7 +4885,7 @@ packages: eslint: 8.31.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy - eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny + eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq eslint-plugin-jest: 26.9.0_ohsifnwenhmxgcp7mend4dnv74 eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0 @@ -6008,17 +6022,18 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: + /@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 @@ -6027,11 +6042,14 @@ packages: '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: + /@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: + '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: @@ -6040,13 +6058,14 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom - codemirror: 6.0.1 + '@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e + codemirror: 6.0.1_@lezer+common@1.0.2 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' + - '@codemirror/lint' - '@codemirror/search' dev: false @@ -7357,16 +7376,18 @@ packages: engines: {node: '>=0.10.0'} dev: false - /codemirror/6.0.1: + /codemirror/6.0.1_@lezer+common@1.0.2: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: - '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu + '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 + transitivePeerDependencies: + - '@lezer/common' dev: false /collection-visit/1.0.0: @@ -8702,7 +8723,7 @@ packages: debug: 4.3.4 enhanced-resolve: 5.12.0 eslint: 8.31.0 - eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny + eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq get-tsconfig: 4.3.0 globby: 13.1.3 is-core-module: 2.11.0 @@ -8712,7 +8733,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq: + /eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -8737,7 +8758,6 @@ packages: debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy transitivePeerDependencies: - supports-color dev: true @@ -8762,7 +8782,7 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-import/2.27.4_2ac3tknkazjoq5fxmuugu665ny: + /eslint-plugin-import/2.27.4_qdjeohovcytra7xto5vgmxssaq: resolution: {integrity: sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==} engines: {node: '>=4'} peerDependencies: @@ -8780,7 +8800,7 @@ packages: doctrine: 2.1.0 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq + eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -13882,15 +13902,6 @@ packages: shallow-equal: 1.2.1 dev: false - /react-dom/18.2.0: - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - scheduler: 0.23.0 - dev: false - /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: From 24b57e026beca2d5406c74076b28aadf568e21b9 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 11:57:15 +0000 Subject: [PATCH 41/71] Added support for verifying webhooks, with WhatsApp working --- .../webhooks/$serviceIdentifier.$id.ts | 61 +++++++++- .../handleExternalSource.server.ts | 67 ++++------ .../app/services/externalSources/utils.ts | 38 ++++++ .../verifyExternalSource.server.ts | 114 ++++++++++++++++++ integrations/github/src/internal/webhooks.ts | 7 ++ .../whatsapp/src/internal/webhooks.ts | 30 ++++- packages/integration-sdk/src/types.ts | 20 +++ 7 files changed, 291 insertions(+), 46 deletions(-) create mode 100644 apps/webapp/app/services/externalSources/utils.ts create mode 100644 apps/webapp/app/services/externalSources/verifyExternalSource.server.ts diff --git a/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts b/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts index d6f99281fd6..79354ac6003 100644 --- a/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts +++ b/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts @@ -1,12 +1,65 @@ -import type { ActionArgs } from "@remix-run/server-runtime"; +import type { ActionArgs, LoaderArgs } from "@remix-run/server-runtime"; import { z } from "zod"; import { findExternalSourceById } from "~/models/externalSource.server"; import { HandleExternalSource } from "~/services/externalSources/handleExternalSource.server"; +import { VerifyExternalSource } from "~/services/externalSources/verifyExternalSource.server"; + +const paramsSchema = z.object({ + id: z.string(), + serviceIdentifier: z.string(), +}); + +export async function loader({ request, params }: LoaderArgs) { + const { id, serviceIdentifier } = paramsSchema.parse(params); + + const externalSource = await findExternalSourceById(id); + + if (!externalSource) { + return { + status: 404, + body: `Could not find external source with id ${id} and serviceIdentifier ${serviceIdentifier}`, + }; + } + + if ( + !externalSource.manualRegistration && + externalSource.connection?.apiIdentifier !== serviceIdentifier + ) { + return { status: 500, body: "Service identifier does not match" }; + } + + try { + const service = new VerifyExternalSource(); + const result = await service.call( + externalSource, + serviceIdentifier, + request + ); + + switch (result.status) { + case "ok": { + return new Response(result.data, { status: 200 }); + } + case "ignored": { + return new Response(result.reason, { status: 200 }); + } + case "error": { + return new Response(result.error, { status: 500 }); + } + } + } catch (error) { + return { + status: 500, + body: + error instanceof Error + ? error.message + : `Unknown error: ${JSON.stringify(error)}`, + }; + } +} export async function action({ request, params }: ActionArgs) { - const { id, serviceIdentifier } = z - .object({ id: z.string(), serviceIdentifier: z.string() }) - .parse(params); + const { id, serviceIdentifier } = paramsSchema.parse(params); const externalSource = await findExternalSourceById(id); diff --git a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts index 87f0229cc9f..0d7cc3ae2a1 100644 --- a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts +++ b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts @@ -1,12 +1,14 @@ -import type { PrismaClient } from "~/db.server"; -import { prisma } from "~/db.server"; +import { ManualWebhookSourceSchema } from "@trigger.dev/common-schemas"; import * as github from "@trigger.dev/github/internal"; -import type { ExternalSourceWithConnection } from "~/models/externalSource.server"; import type { NormalizedRequest } from "@trigger.dev/integration-sdk"; -import { IngestEvent } from "../events/ingest.server"; -import { ManualWebhookSourceSchema } from "@trigger.dev/common-schemas"; +import * as whatsapp from "@trigger.dev/whatsapp/internal"; import { createHmac, timingSafeEqual } from "node:crypto"; import { ulid } from "ulid"; +import type { PrismaClient } from "~/db.server"; +import { prisma } from "~/db.server"; +import type { ExternalSourceWithConnection } from "~/models/externalSource.server"; +import { IngestEvent } from "../events/ingest.server"; +import { createNormalizedRequest } from "./utils"; type IgnoredEventResponse = { status: "ignored"; @@ -41,35 +43,12 @@ export class HandleExternalSource { this.#prismaClient = prismaClient; } - async #createNormalizedRequest(request: Request): Promise { - const requestUrl = new URL(request.url); - const rawSearchParams = requestUrl.searchParams; - const rawBody = await request.text(); - const rawHeaders = Object.fromEntries(request.headers.entries()); - - return { - rawBody, - body: this.#safeJsonParse(rawBody), - headers: rawHeaders, - searchParams: rawSearchParams, - }; - } - - #safeJsonParse(json: string): any { - try { - return JSON.parse(json); - } catch (error) { - return null; - } - } - public async call( externalSource: NonNullable, serviceIdentifier: string, request: Request ) { - const normalizedRequest = await this.#createNormalizedRequest(request); - + const normalizedRequest = await createNormalizedRequest(request); const possibleEvent = await this.#handleExternalSource( externalSource, serviceIdentifier, @@ -135,14 +114,6 @@ export class HandleExternalSource { serviceIdentifier: string, request: NormalizedRequest ): Promise { - if (externalSource.manualRegistration) { - return this.#handleManualWebhook( - externalSource, - serviceIdentifier, - request - ); - } - switch (serviceIdentifier) { case "github": { return github.internalIntegration.webhooks!.handleWebhookRequest({ @@ -150,12 +121,26 @@ export class HandleExternalSource { secret: externalSource.secret ?? undefined, }); } - default: { - throw new Error( - `Could not handle webhook with unsupported service identifier: ${serviceIdentifier}` - ); + case "whatsapp": { + return whatsapp.internalIntegration.webhooks!.handleWebhookRequest({ + request, + secret: externalSource.secret ?? undefined, + }); } } + + if (externalSource.manualRegistration) { + return this.#handleManualWebhook( + externalSource, + serviceIdentifier, + request + ); + } + + return { + status: "ignored" as const, + reason: `Could not handle external source with unsupported service: ${serviceIdentifier}`, + }; } async #handleManualWebhook( diff --git a/apps/webapp/app/services/externalSources/utils.ts b/apps/webapp/app/services/externalSources/utils.ts new file mode 100644 index 00000000000..6b486f37a44 --- /dev/null +++ b/apps/webapp/app/services/externalSources/utils.ts @@ -0,0 +1,38 @@ +import type { + HTTPMethod, + NormalizedRequest, +} from "@trigger.dev/integration-sdk"; +import { httpMethods } from "@trigger.dev/integration-sdk"; + +export async function createNormalizedRequest( + request: Request +): Promise { + const requestUrl = new URL(request.url); + const rawSearchParams = requestUrl.searchParams; + const rawBody = await request.text(); + const rawHeaders = Object.fromEntries(request.headers.entries()); + + if (!isMethod(request.method)) { + throw new Error(`Invalid method: ${request.method}`); + } + + return { + rawBody, + body: safeJsonParse(rawBody), + headers: rawHeaders, + searchParams: rawSearchParams, + method: request.method, + }; +} + +function safeJsonParse(json: string): any { + try { + return JSON.parse(json); + } catch (error) { + return null; + } +} + +function isMethod(str: string): str is HTTPMethod { + return !!httpMethods.find((method) => str === method); +} diff --git a/apps/webapp/app/services/externalSources/verifyExternalSource.server.ts b/apps/webapp/app/services/externalSources/verifyExternalSource.server.ts new file mode 100644 index 00000000000..163b2b5ad28 --- /dev/null +++ b/apps/webapp/app/services/externalSources/verifyExternalSource.server.ts @@ -0,0 +1,114 @@ +import * as github from "@trigger.dev/github/internal"; +import type { NormalizedRequest } from "@trigger.dev/integration-sdk"; +import * as whatsapp from "@trigger.dev/whatsapp/internal"; +import type { PrismaClient } from "~/db.server"; +import { prisma } from "~/db.server"; +import type { ExternalSourceWithConnection } from "~/models/externalSource.server"; +import { createNormalizedRequest } from "./utils"; + +type ErrorEventResponse = { + status: "error"; + error: string; +}; +type IgnoredEventResponse = { + status: "ignored"; + reason: string; +}; + +type TriggeredEventResponse = { + status: "ok"; + data: any; +}; + +export type VerifyExternalEventResponse = + | TriggeredEventResponse + | IgnoredEventResponse + | ErrorEventResponse; + +export class VerifyExternalSource { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call( + externalSource: NonNullable, + serviceIdentifier: string, + request: Request + ): Promise { + const normalizedRequest = await createNormalizedRequest(request); + return this.#verifyExternalSource( + externalSource, + serviceIdentifier, + normalizedRequest + ); + } + + async #verifyExternalSource( + externalSource: NonNullable, + serviceIdentifier: string, + normalizedRequest: NormalizedRequest + ): Promise { + switch (externalSource.type) { + case "WEBHOOK": { + return this.#verifyWebhook( + externalSource, + serviceIdentifier, + normalizedRequest + ); + } + default: { + return { + status: "error", + error: `Could not handle external source with unsupported type: ${externalSource.type}`, + }; + } + } + } + + async #verifyWebhook( + externalSource: NonNullable, + serviceIdentifier: string, + request: NormalizedRequest + ): Promise { + switch (serviceIdentifier) { + case "github": { + return github.internalIntegration.webhooks!.verifyWebhookRequest({ + request, + secret: externalSource.secret ?? undefined, + }); + } + case "whatsapp": { + return whatsapp.internalIntegration.webhooks!.verifyWebhookRequest({ + request, + secret: externalSource.secret ?? undefined, + }); + } + } + + if (externalSource.manualRegistration) { + return this.#handleManualWebhook( + externalSource, + serviceIdentifier, + request + ); + } + + return { + status: "ignored" as const, + reason: `Could not handle external source with unsupported service: ${serviceIdentifier}`, + }; + } + + async #handleManualWebhook( + externalSource: NonNullable, + serviceIdentifier: string, + request: NormalizedRequest + ): Promise { + return { + status: "ok" as const, + data: undefined, + }; + } +} diff --git a/integrations/github/src/internal/webhooks.ts b/integrations/github/src/internal/webhooks.ts index 24dde60e1ba..c9d97ac92ad 100644 --- a/integrations/github/src/internal/webhooks.ts +++ b/integrations/github/src/internal/webhooks.ts @@ -39,6 +39,13 @@ export class GitHubWebhookIntegration implements WebhookIntegration { } } + verifyWebhookRequest(options: HandleWebhookOptions) { + return { + status: "ok" as const, + data: undefined, + }; + } + handleWebhookRequest(options: HandleWebhookOptions) { const deliveryId = options.request.headers["x-github-delivery"]; const hookId = options.request.headers["x-github-hook-id"]; diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts index 5946c6db9a3..18f949aab5d 100644 --- a/integrations/whatsapp/src/internal/webhooks.ts +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -19,12 +19,40 @@ export class WhatsAppWebhookIntegration implements WebhookIntegration { return Promise.reject("Not implemented"); } + verifyWebhookRequest(options: HandleWebhookOptions) { + if (!options.request.searchParams.has("hub.verify_token")) { + return { + status: "ignored" as const, + reason: "Missing hub.verify_token", + }; + } + + if ( + options.secret !== options.request.searchParams.get("hub.verify_token") + ) { + return { + status: "error" as const, + error: "Invalid secret", + }; + } + + return { + status: "ok" as const, + data: options.request.searchParams.get("hub.challenge"), + }; + } + handleWebhookRequest(options: HandleWebhookOptions) { console.log("Handling WhatsApp webhook request", options.request.body); return { status: "ok" as const, - data: { id: "", payload: options.request.body, event: "", context: {} }, + data: { + id: "", + payload: options.request.body, + event: "", + context: {}, + }, }; } diff --git a/packages/integration-sdk/src/types.ts b/packages/integration-sdk/src/types.ts index 2d4239df000..c10f9345e87 100644 --- a/packages/integration-sdk/src/types.ts +++ b/packages/integration-sdk/src/types.ts @@ -12,11 +12,25 @@ export interface WebhookConfig { secret: string; } +export const httpMethods = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS", + "TRACE", + "CONNECT", +]; +export type HTTPMethod = (typeof httpMethods)[number]; + export interface NormalizedRequest { rawBody: string; body: any; headers: Record; searchParams: URLSearchParams; + method: HTTPMethod; } export interface NormalizedResponse { @@ -74,6 +88,12 @@ export interface WebhookIntegration { | { status: "ok"; data: ReceivedWebhook } | { status: "ignored"; reason: string } | { status: "error"; error: string }; + verifyWebhookRequest: ( + options: HandleWebhookOptions + ) => + | { status: "ok"; data: any } + | { status: "ignored"; reason: string } + | { status: "error"; error: string }; displayProperties: (source: unknown) => DisplayProperties; } From 6e8d9b4c9a5a40473785d9bc531ee8b67dc53a67 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 13:14:47 +0000 Subject: [PATCH 42/71] WIP on webhook verification --- config-packages/tsconfig/examples.json | 6 ++ examples/whatsapp/package.json | 19 +++++ examples/whatsapp/src/index.ts | 18 ++++ examples/whatsapp/tsconfig.json | 5 ++ integrations/whatsapp/package.json | 3 +- integrations/whatsapp/src/events.ts | 24 ++++-- .../whatsapp/src/internal/webhooks.ts | 82 +++++++++++++++++-- integrations/whatsapp/src/schemas/index.ts | 2 + pnpm-lock.yaml | 19 +++++ 9 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 examples/whatsapp/package.json create mode 100644 examples/whatsapp/src/index.ts create mode 100644 examples/whatsapp/tsconfig.json diff --git a/config-packages/tsconfig/examples.json b/config-packages/tsconfig/examples.json index 6426fe4eaa9..20c484b39c5 100644 --- a/config-packages/tsconfig/examples.json +++ b/config-packages/tsconfig/examples.json @@ -47,6 +47,12 @@ ], "@trigger.dev/shopify/*": [ "../../integrations/shopify/src/*" + ], + "@trigger.dev/whatsapp": [ + "../../integrations/whatsapp/src/index" + ], + "@trigger.dev/whatsapp/*": [ + "../../integrations/whatsapp/src/*" ] } } diff --git a/examples/whatsapp/package.json b/examples/whatsapp/package.json new file mode 100644 index 00000000000..5f74d45c81a --- /dev/null +++ b/examples/whatsapp/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "name": "@examples/whatsapp", + "version": "0.0.1", + "description": "Example trigger.dev workflow that uses the WhatsApp Webhook integration", + "dependencies": { + "@trigger.dev/whatsapp": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "zod": "^3.20.2" + }, + "devDependencies": { + "@trigger.dev/tsconfig": "workspace:*", + "@types/node": "16", + "tsx": "^3.12.0" + }, + "scripts": { + "dev": "tsx src/index.ts" + } +} diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts new file mode 100644 index 00000000000..d1f16c4bff1 --- /dev/null +++ b/examples/whatsapp/src/index.ts @@ -0,0 +1,18 @@ +import { Trigger } from "@trigger.dev/sdk"; +import { events } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "whatsapp-webhook", + name: "WhatsApp webhook", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: events.messageEvent({ + accountId: "114848614845931", + }), + run: async (event, ctx) => { + await ctx.logger.info(`Action was ${event.action}`); + + return {}; + }, +}).listen(); diff --git a/examples/whatsapp/tsconfig.json b/examples/whatsapp/tsconfig.json new file mode 100644 index 00000000000..00ad22f1a85 --- /dev/null +++ b/examples/whatsapp/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@trigger.dev/tsconfig/examples.json", + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "**/*.test.*"] +} diff --git a/integrations/whatsapp/package.json b/integrations/whatsapp/package.json index d55816a6c84..ef278716244 100644 --- a/integrations/whatsapp/package.json +++ b/integrations/whatsapp/package.json @@ -24,9 +24,9 @@ "dist/index.js.map" ], "devDependencies": { + "@trigger.dev/integration-sdk": "workspace:*", "@trigger.dev/sdk": "workspace:*", "@trigger.dev/tsconfig": "workspace:*", - "@trigger.dev/integration-sdk": "workspace:*", "@types/node": "16", "rimraf": "^3.0.2", "tsup": "^6.5.0" @@ -40,6 +40,7 @@ "build:tsup": "tsup" }, "dependencies": { + "ulid": "^2.3.0", "zod": "^3.20.2" } } diff --git a/integrations/whatsapp/src/events.ts b/integrations/whatsapp/src/events.ts index d9d3cb79ef6..c38b2fc57e3 100644 --- a/integrations/whatsapp/src/events.ts +++ b/integrations/whatsapp/src/events.ts @@ -2,24 +2,34 @@ import type { TriggerEvent } from "@trigger.dev/sdk"; import * as schemas from "./schemas"; export function messageEvent(params: { - repo: string; + accountId: string; }): TriggerEvent { return { metadata: { type: "WEBHOOK", service: "whatsapp", - name: "message", + name: "messages", filter: { service: ["whatsapp"], - payload: {}, - event: ["message"], + payload: { + object: ["whatsapp_business_account"], + entry: { + id: [params.accountId], + changes: { + field: ["messages"], + }, + }, + }, + event: ["messages"], }, - source: { + source: schemas.WebhookSourceSchema.parse({ + subresource: "messages", + accountId: params.accountId, verifyPayload: { enabled: true, }, - event: "message", - }, + event: "messages", + }), manualRegistration: true, }, schema: schemas.messageEvents.messageEventSchema, diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts index 18f949aab5d..878a83150d8 100644 --- a/integrations/whatsapp/src/internal/webhooks.ts +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import crypto from "crypto"; +import { ulid } from "ulid"; import { getAccessToken } from "@trigger.dev/integration-sdk"; import type { DisplayProperty, @@ -11,8 +12,15 @@ import { WebhookSourceSchema } from "../schemas"; export class WhatsAppWebhookIntegration implements WebhookIntegration { keyForSource(source: unknown): string { const whatsAppSource = parseWebhookSource(source); - //todo add some identifier in here for the WhatsApp account/number - return `messages.${whatsAppSource.events.join(".")}`; + + switch (whatsAppSource.subresource) { + case "messages": + return `messages.${ + whatsAppSource.accountId + }.${whatsAppSource.events.join(".")}`; + default: + throw new Error(`Unknown subresource`); + } } registerWebhook(config: WebhookConfig, source: unknown) { @@ -43,15 +51,58 @@ export class WhatsAppWebhookIntegration implements WebhookIntegration { } handleWebhookRequest(options: HandleWebhookOptions) { - console.log("Handling WhatsApp webhook request", options.request.body); + console.log( + "Handling WhatsApp search params", + options.request.searchParams.toString() + ); + console.log( + "Handling WhatsApp headers", + JSON.stringify(options.request.headers) + ); + + console.log( + "Handling WhatsApp webhook request", + JSON.stringify(options.request.body) + ); + + if (!options.secret) { + return { + status: "error" as const, + error: "Missing secret", + }; + } + + const calculatedChecksum = crypto + .createHmac("sha256", options.secret) + .update(options.request.rawBody) + .digest("hex"); + + const expected = + options.request.headers["x-hub-signature-256"].split("=")[1]; + if (expected !== calculatedChecksum) { + return { + status: "error" as const, + error: "Invalid signature", + }; + } + + const context = omit(options.request.headers, [ + "x-hub-signature-256", + "x-hub-signature", + "content-type", + "content-length", + "accept", + "accept-encoding", + "x-forwarded-proto", + ]); return { status: "ok" as const, data: { - id: "", + id: ulid(), payload: options.request.body, - event: "", - context: {}, + event: "messages", + context, }, }; } @@ -66,3 +117,20 @@ export const webhooks = new WhatsAppWebhookIntegration(); function parseWebhookSource(source: unknown) { return WebhookSourceSchema.parse(source); } + +function handleMessagesEvent(options: HandleWebhookOptions) {} + +function omit, K extends keyof T>( + obj: T, + keys: K[] +): Omit { + const result: any = {}; + + for (const key of Object.keys(obj)) { + if (!keys.includes(key as K)) { + result[key] = obj[key]; + } + } + + return result; +} diff --git a/integrations/whatsapp/src/schemas/index.ts b/integrations/whatsapp/src/schemas/index.ts index 7b341211bd6..dab78be3acc 100644 --- a/integrations/whatsapp/src/schemas/index.ts +++ b/integrations/whatsapp/src/schemas/index.ts @@ -2,6 +2,8 @@ import { z } from "zod"; import * as messageEvents from "./messageEvents"; export const WebhookSourceSchema = z.object({ + subresource: z.literal("messages"), + accountId: z.string(), scopes: z.array(z.string()), events: z.array(z.string()), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c47d6c3fd67..a5e31490636 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,6 +563,23 @@ importers: '@types/node': 16.18.11 tsx: 3.12.2 + examples/whatsapp: + specifiers: + '@trigger.dev/sdk': workspace:* + '@trigger.dev/tsconfig': workspace:* + '@trigger.dev/whatsapp': workspace:* + '@types/node': '16' + tsx: ^3.12.0 + zod: ^3.20.2 + dependencies: + '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/whatsapp': link:../../integrations/whatsapp + zod: 3.20.2 + devDependencies: + '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/node': 16.18.11 + tsx: 3.12.2 + integrations/github: specifiers: '@octokit/webhooks': ^10.4.0 @@ -677,8 +694,10 @@ importers: '@types/node': '16' rimraf: ^3.0.2 tsup: ^6.5.0 + ulid: ^2.3.0 zod: ^3.20.2 dependencies: + ulid: 2.3.0 zod: 3.20.2 devDependencies: '@trigger.dev/integration-sdk': link:../../packages/integration-sdk From c57889194733f35e694841491993409c209deebc Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 14:42:23 +0000 Subject: [PATCH 43/71] Removed webhook payload validation --- .../whatsapp/src/internal/webhooks.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts index 878a83150d8..9b807a73ab2 100644 --- a/integrations/whatsapp/src/internal/webhooks.ts +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -65,27 +65,6 @@ export class WhatsAppWebhookIntegration implements WebhookIntegration { JSON.stringify(options.request.body) ); - if (!options.secret) { - return { - status: "error" as const, - error: "Missing secret", - }; - } - - const calculatedChecksum = crypto - .createHmac("sha256", options.secret) - .update(options.request.rawBody) - .digest("hex"); - - const expected = - options.request.headers["x-hub-signature-256"].split("=")[1]; - if (expected !== calculatedChecksum) { - return { - status: "error" as const, - error: "Invalid signature", - }; - } - const context = omit(options.request.headers, [ "x-hub-signature-256", "x-hub-signature", From 950138bd72addfa1a167402fa8387fd144463e8a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 16:02:02 +0000 Subject: [PATCH 44/71] More work on WhatsApp messages --- apps/webapp/app/models/workflowRun.server.ts | 1 + .../app/services/events/dispatch.server.ts | 2 +- .../handleExternalSource.server.ts | 56 ++++++++++--------- examples/whatsapp/src/index.ts | 2 +- integrations/github/src/internal/webhooks.ts | 2 +- integrations/whatsapp/src/events.ts | 8 +-- .../whatsapp/src/internal/webhooks.ts | 55 ++++++++++++++---- integrations/whatsapp/src/schemas/index.ts | 6 +- .../whatsapp/src/schemas/messageEvents.ts | 40 ++++++++++++- packages/integration-sdk/src/types.ts | 2 +- 10 files changed, 123 insertions(+), 51 deletions(-) diff --git a/apps/webapp/app/models/workflowRun.server.ts b/apps/webapp/app/models/workflowRun.server.ts index 123c6c9fb7a..e824dc5fbe1 100644 --- a/apps/webapp/app/models/workflowRun.server.ts +++ b/apps/webapp/app/models/workflowRun.server.ts @@ -48,6 +48,7 @@ export async function startWorkflowRun(id: string, apiKey: string) { await resolveDisconnectionStepsInRun(workflowRun.id); } else { + console.log(`[startWorkflowRun] ${workflowRun.id} is running`); await prisma.workflowRun.update({ where: { id: workflowRun.id }, data: { diff --git a/apps/webapp/app/services/events/dispatch.server.ts b/apps/webapp/app/services/events/dispatch.server.ts index f547d2efca6..6f828ec5792 100644 --- a/apps/webapp/app/services/events/dispatch.server.ts +++ b/apps/webapp/app/services/events/dispatch.server.ts @@ -22,7 +22,7 @@ export class DispatchEvent { }); if (!event) { - throw new Error("Event not found"); + throw new Error(`Event not found: ${id}`); } console.log( diff --git a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts index 0d7cc3ae2a1..35e08b7e684 100644 --- a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts +++ b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts @@ -28,7 +28,7 @@ type TriggeredEventResponse = { event: string; timestamp?: string; context?: any; - }; + }[]; }; export type HandledExternalEventResponse = @@ -57,28 +57,30 @@ export class HandleExternalSource { switch (possibleEvent.status) { case "ok": { - const { id, payload, event, timestamp, context } = possibleEvent.data; - - const ingestService = new IngestEvent(); - - await ingestService.call( - { - id, - payload, - name: event, - type: externalSource.type, - service: serviceIdentifier, - timestamp, - context, - }, - externalSource.organization - ); + for (let index = 0; index < possibleEvent.data.length; index++) { + const { id, payload, event, timestamp, context } = + possibleEvent.data[index]; + + const ingestService = new IngestEvent(); + + await ingestService.call( + { + id, + payload, + name: event, + type: externalSource.type, + service: serviceIdentifier, + timestamp, + context, + }, + externalSource.organization + ); + } return true; } case "ignored": { console.log(`Ignored external event: ${possibleEvent.reason}`); - return true; } case "error": { @@ -175,15 +177,17 @@ export class HandleExternalSource { return { status: "ok", - data: { - id: ulid(), - payload: request.body, - event: source.event, - context: { - headers: request.headers, - externalSourceId: externalSource.id, + data: [ + { + id: ulid(), + payload: request.body, + event: source.event, + context: { + headers: request.headers, + externalSourceId: externalSource.id, + }, }, - }, + ], }; } } diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index d1f16c4bff1..1b46b17b3e2 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -11,7 +11,7 @@ new Trigger({ accountId: "114848614845931", }), run: async (event, ctx) => { - await ctx.logger.info(`Action was ${event.action}`); + await ctx.logger.info(`Action was ${event}`); return {}; }, diff --git a/integrations/github/src/internal/webhooks.ts b/integrations/github/src/internal/webhooks.ts index c9d97ac92ad..672294f3c6d 100644 --- a/integrations/github/src/internal/webhooks.ts +++ b/integrations/github/src/internal/webhooks.ts @@ -87,7 +87,7 @@ export class GitHubWebhookIntegration implements WebhookIntegration { return { status: "ok" as const, - data: { id, payload: options.request.body, event, context }, + data: [{ id, payload: options.request.body, event, context }], }; } diff --git a/integrations/whatsapp/src/events.ts b/integrations/whatsapp/src/events.ts index c38b2fc57e3..308653dd7ee 100644 --- a/integrations/whatsapp/src/events.ts +++ b/integrations/whatsapp/src/events.ts @@ -12,13 +12,7 @@ export function messageEvent(params: { filter: { service: ["whatsapp"], payload: { - object: ["whatsapp_business_account"], - entry: { - id: [params.accountId], - changes: { - field: ["messages"], - }, - }, + type: ["message"], }, event: ["messages"], }, diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts index 9b807a73ab2..f6b95ee4227 100644 --- a/integrations/whatsapp/src/internal/webhooks.ts +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -1,6 +1,6 @@ import crypto from "crypto"; import { ulid } from "ulid"; -import { getAccessToken } from "@trigger.dev/integration-sdk"; +import { getAccessToken, ReceivedWebhook } from "@trigger.dev/integration-sdk"; import type { DisplayProperty, HandleWebhookOptions, @@ -15,9 +15,7 @@ export class WhatsAppWebhookIntegration implements WebhookIntegration { switch (whatsAppSource.subresource) { case "messages": - return `messages.${ - whatsAppSource.accountId - }.${whatsAppSource.events.join(".")}`; + return `messages.${whatsAppSource.accountId}.${whatsAppSource.event}`; default: throw new Error(`Unknown subresource`); } @@ -75,14 +73,11 @@ export class WhatsAppWebhookIntegration implements WebhookIntegration { "x-forwarded-proto", ]); + const data = getData(options.request.body, context); + return { status: "ok" as const, - data: { - id: ulid(), - payload: options.request.body, - event: "messages", - context, - }, + data, }; } @@ -97,7 +92,45 @@ function parseWebhookSource(source: unknown) { return WebhookSourceSchema.parse(source); } -function handleMessagesEvent(options: HandleWebhookOptions) {} +function getData( + body: any, + context: Record +): ReceivedWebhook[] { + const webhooks: ReceivedWebhook[] = []; + for (const entry of body.entry) { + for (const change of entry.changes) { + if (change.field === "messages") { + const messageData = change.value; + + const metadata = messageData.metadata; + const contacts = messageData.contacts; + + for (const message of messageData.messages) { + const timestamp = `${message.timestamp}000`; + webhooks.push({ + id: message.id as string, + payload: { + type: "message", + contacts, + metadata, + message: { + ...message, + timestamp, + }, + }, + timestamp, + event: "messages", + context, + }); + } + } else { + console.error(`Unknown field ${change.field}`); + } + } + } + + return webhooks; +} function omit, K extends keyof T>( obj: T, diff --git a/integrations/whatsapp/src/schemas/index.ts b/integrations/whatsapp/src/schemas/index.ts index dab78be3acc..94516251b26 100644 --- a/integrations/whatsapp/src/schemas/index.ts +++ b/integrations/whatsapp/src/schemas/index.ts @@ -4,8 +4,10 @@ import * as messageEvents from "./messageEvents"; export const WebhookSourceSchema = z.object({ subresource: z.literal("messages"), accountId: z.string(), - scopes: z.array(z.string()), - events: z.array(z.string()), + event: z.string(), + verifyPayload: z.object({ + enabled: z.boolean(), + }), }); export { messageEvents }; diff --git a/integrations/whatsapp/src/schemas/messageEvents.ts b/integrations/whatsapp/src/schemas/messageEvents.ts index fdc5a9af737..8f31577cb1f 100644 --- a/integrations/whatsapp/src/schemas/messageEvents.ts +++ b/integrations/whatsapp/src/schemas/messageEvents.ts @@ -1,3 +1,41 @@ import { z } from "zod"; -export const messageEventSchema = z.any(); +const metadataSchema = z.object({ + display_phone_number: z.string(), + phone_number_id: z.string(), +}); + +const contactSchema = z.object({ + profile: z.object({ name: z.string() }), + wa_id: z.string(), +}); + +const commonMessageData = z.object({ + id: z.string(), + from: z.string(), + timestamp: z.date(), +}); + +const textMessageEventSchema = z.object({ + type: z.literal("text"), + text: z.object({ body: z.string() }), +}); + +const audioMessageEventSchema = z.object({ + type: z.literal("audio"), + audio: z.object({ + id: z.string(), + mime_type: z.string(), + }), +}); + +const messageSchema = z + .discriminatedUnion("type", [textMessageEventSchema, audioMessageEventSchema]) + .and(commonMessageData); + +export const messageEventSchema = z.object({ + type: z.literal("message"), + contacts: z.array(contactSchema), + metadata: metadataSchema, + message: messageSchema, +}); diff --git a/packages/integration-sdk/src/types.ts b/packages/integration-sdk/src/types.ts index c10f9345e87..d9eeea717da 100644 --- a/packages/integration-sdk/src/types.ts +++ b/packages/integration-sdk/src/types.ts @@ -85,7 +85,7 @@ export interface WebhookIntegration { handleWebhookRequest: ( options: HandleWebhookOptions ) => - | { status: "ok"; data: ReceivedWebhook } + | { status: "ok"; data: ReceivedWebhook[] } | { status: "ignored"; reason: string } | { status: "error"; error: string }; verifyWebhookRequest: ( From 9a529492d04865f54d5be7607ce59ea8c30dff10 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 16:24:05 +0000 Subject: [PATCH 45/71] WhatsApp text messages are coming through --- examples/whatsapp/src/index.ts | 3 ++- integrations/whatsapp/src/events.ts | 4 +--- integrations/whatsapp/src/internal/webhooks.ts | 3 +-- integrations/whatsapp/src/schemas/messageEvents.ts | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 1b46b17b3e2..914d0c88378 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -11,7 +11,8 @@ new Trigger({ accountId: "114848614845931", }), run: async (event, ctx) => { - await ctx.logger.info(`Action was ${event}`); + await ctx.logger.info(`Message data`, event.message); + await ctx.logger.info(`Phone number`, event.contacts[0]); return {}; }, diff --git a/integrations/whatsapp/src/events.ts b/integrations/whatsapp/src/events.ts index 308653dd7ee..11ef09e22db 100644 --- a/integrations/whatsapp/src/events.ts +++ b/integrations/whatsapp/src/events.ts @@ -11,9 +11,7 @@ export function messageEvent(params: { name: "messages", filter: { service: ["whatsapp"], - payload: { - type: ["message"], - }, + payload: {}, event: ["messages"], }, source: schemas.WebhookSourceSchema.parse({ diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts index f6b95ee4227..3ce304844d2 100644 --- a/integrations/whatsapp/src/internal/webhooks.ts +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -115,10 +115,9 @@ function getData( metadata, message: { ...message, - timestamp, + timestamp: parseInt(timestamp), }, }, - timestamp, event: "messages", context, }); diff --git a/integrations/whatsapp/src/schemas/messageEvents.ts b/integrations/whatsapp/src/schemas/messageEvents.ts index 8f31577cb1f..94598548020 100644 --- a/integrations/whatsapp/src/schemas/messageEvents.ts +++ b/integrations/whatsapp/src/schemas/messageEvents.ts @@ -13,7 +13,7 @@ const contactSchema = z.object({ const commonMessageData = z.object({ id: z.string(), from: z.string(), - timestamp: z.date(), + timestamp: z.coerce.date(), }); const textMessageEventSchema = z.object({ From 6acda311dbf244a8ac23cf071ce4f4a26f0a8dbe Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 31 Jan 2023 16:48:21 +0000 Subject: [PATCH 46/71] Added lots of message types to the schemas --- .../whatsapp/src/schemas/messageEvents.ts | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/integrations/whatsapp/src/schemas/messageEvents.ts b/integrations/whatsapp/src/schemas/messageEvents.ts index 94598548020..ebf89b9edc8 100644 --- a/integrations/whatsapp/src/schemas/messageEvents.ts +++ b/integrations/whatsapp/src/schemas/messageEvents.ts @@ -14,6 +14,12 @@ const commonMessageData = z.object({ id: z.string(), from: z.string(), timestamp: z.coerce.date(), + context: z + .object({ + id: z.string(), + from: z.string(), + }) + .optional(), }); const textMessageEventSchema = z.object({ @@ -26,11 +32,126 @@ const audioMessageEventSchema = z.object({ audio: z.object({ id: z.string(), mime_type: z.string(), + sha256: z.string().optional(), + voice: z.boolean().optional(), }), }); +const videoMessageEventSchema = z.object({ + type: z.literal("video"), + video: z.object({ + id: z.string(), + mime_type: z.string(), + sha256: z.string().optional(), + }), +}); + +const imageMessageEventSchema = z.object({ + type: z.literal("image"), + image: z.object({ + id: z.string(), + mime_type: z.string(), + sha256: z.string().optional(), + caption: z.string().optional(), + }), +}); + +const reactionMessageEventSchema = z.object({ + type: z.literal("reaction"), + reaction: z.object({ + emoji: z.string(), + message_id: z.string(), + }), +}); + +const stickerMessageEventSchema = z.object({ + type: z.literal("sticker"), + sticker: z.object({ + id: z.string(), + sha256: z.string(), + animated: z.boolean(), + mime_type: z.string(), + }), +}); + +const documentMessageEventSchema = z.object({ + type: z.literal("document"), + document: z.object({ + id: z.string(), + mime_type: z.string(), + sha256: z.string().optional(), + filename: z.string().optional(), + caption: z.string().optional(), + }), +}); + +const locationMessageEventSchema = z.object({ + type: z.literal("location"), + location: z.object({ + latitude: z.number(), + longitude: z.number(), + name: z.string().optional(), + address: z.string().optional(), + url: z.string().optional(), + }), +}); + +const sharedContactSchema = z.object({ + name: z + .object({ + last_name: z.string().optional(), + first_name: z.string().optional(), + formatted_name: z.string().optional(), + }) + .optional(), + emails: z + .array(z.object({ type: z.string().optional(), email: z.string() })) + .optional(), + phones: z + .array( + z.object({ + type: z.string().optional(), + phone: z.string(), + wa_id: z.string().optional(), + }) + ) + .optional(), + birthday: z.string().optional(), + addresses: z + .array( + z.object({ + zip: z.string().optional(), + city: z.string().optional(), + type: z.string().optional(), + street: z.string().optional(), + }) + ) + .optional(), +}); + +const contactsMessageEventSchema = z.object({ + type: z.literal("contacts"), + contacts: z.array(sharedContactSchema), +}); + +const unsupportedMessageEventSchema = z.object({ + type: z.literal("unsupported"), + errors: z.array(z.object({ code: z.number(), title: z.string() })), +}); + const messageSchema = z - .discriminatedUnion("type", [textMessageEventSchema, audioMessageEventSchema]) + .discriminatedUnion("type", [ + textMessageEventSchema, + audioMessageEventSchema, + imageMessageEventSchema, + videoMessageEventSchema, + reactionMessageEventSchema, + stickerMessageEventSchema, + documentMessageEventSchema, + locationMessageEventSchema, + contactsMessageEventSchema, + unsupportedMessageEventSchema, + ]) .and(commonMessageData); export const messageEventSchema = z.object({ From c1511f233ca8a1a165027692e03eb20e8f16d880 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 10:36:34 +0000 Subject: [PATCH 47/71] Match the connection using the slug --- apps/webapp/app/routes/resources/connection/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/resources/connection/index.tsx b/apps/webapp/app/routes/resources/connection/index.tsx index 01f3b73f035..d758c9d6a6c 100644 --- a/apps/webapp/app/routes/resources/connection/index.tsx +++ b/apps/webapp/app/routes/resources/connection/index.tsx @@ -92,7 +92,7 @@ export const action = async ({ request }: ActionArgs) => { const parsed = parsedResult.data; const integrationInfo = getIntegrations(true).find( - (i) => i.slug === parsed.service + (i) => i.metadata.slug === parsed.service ); if (!integrationInfo) { throw new Error("Integration not found"); @@ -109,7 +109,7 @@ export const action = async ({ request }: ActionArgs) => { .filter((connection) => connection.apiIdentifier === parsed.service) .sort((a, b) => a.title.localeCompare(b.title)); - let title = integrationInfo.name; + let title = integrationInfo.metadata.name; if (connectionsForApiIdentifier.length > 0) { title += ` #${connectionsForApiIdentifier.length + 1}`; } From 69004cdcf4fcf097ba2b5b99b06312e595e3ce79 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 10:42:33 +0000 Subject: [PATCH 48/71] Revert "Match the connection using the slug" This reverts commit c1511f233ca8a1a165027692e03eb20e8f16d880. --- apps/webapp/app/routes/resources/connection/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/resources/connection/index.tsx b/apps/webapp/app/routes/resources/connection/index.tsx index d758c9d6a6c..01f3b73f035 100644 --- a/apps/webapp/app/routes/resources/connection/index.tsx +++ b/apps/webapp/app/routes/resources/connection/index.tsx @@ -92,7 +92,7 @@ export const action = async ({ request }: ActionArgs) => { const parsed = parsedResult.data; const integrationInfo = getIntegrations(true).find( - (i) => i.metadata.slug === parsed.service + (i) => i.slug === parsed.service ); if (!integrationInfo) { throw new Error("Integration not found"); @@ -109,7 +109,7 @@ export const action = async ({ request }: ActionArgs) => { .filter((connection) => connection.apiIdentifier === parsed.service) .sort((a, b) => a.title.localeCompare(b.title)); - let title = integrationInfo.metadata.name; + let title = integrationInfo.name; if (connectionsForApiIdentifier.length > 0) { title += ` #${connectionsForApiIdentifier.length + 1}`; } From dbfff6b5f4430f31aad93f9be88ff9d2d426136e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 10:44:09 +0000 Subject: [PATCH 49/71] WhatsApp send template message --- examples/whatsapp/src/index.ts | 9 +- integrations/whatsapp/package.json | 6 +- integrations/whatsapp/src/index.ts | 34 +++- integrations/whatsapp/src/internal.ts | 3 + .../whatsapp/src/internal/requests.ts | 178 ++++++++++++++++++ integrations/whatsapp/src/schemas/index.ts | 3 +- integrations/whatsapp/src/schemas/messages.ts | 134 +++++++++++++ pnpm-lock.yaml | 4 + 8 files changed, 365 insertions(+), 6 deletions(-) create mode 100644 integrations/whatsapp/src/internal/requests.ts create mode 100644 integrations/whatsapp/src/schemas/messages.ts diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 914d0c88378..e611bafb74c 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -1,5 +1,5 @@ import { Trigger } from "@trigger.dev/sdk"; -import { events } from "@trigger.dev/whatsapp"; +import { events, sendTemplate } from "@trigger.dev/whatsapp"; new Trigger({ id: "whatsapp-webhook", @@ -14,6 +14,11 @@ new Trigger({ await ctx.logger.info(`Message data`, event.message); await ctx.logger.info(`Phone number`, event.contacts[0]); - return {}; + const response = await sendTemplate("template-msg", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + template: "hello_world", + languageCode: "en_US", + }); }, }).listen(); diff --git a/integrations/whatsapp/package.json b/integrations/whatsapp/package.json index ef278716244..b10421b9809 100644 --- a/integrations/whatsapp/package.json +++ b/integrations/whatsapp/package.json @@ -29,7 +29,8 @@ "@trigger.dev/tsconfig": "workspace:*", "@types/node": "16", "rimraf": "^3.0.2", - "tsup": "^6.5.0" + "tsup": "^6.5.0", + "@types/debug": "^4.1.7" }, "peerDependencies": { "@trigger.dev/sdk": "workspace:*" @@ -41,6 +42,7 @@ }, "dependencies": { "ulid": "^2.3.0", - "zod": "^3.20.2" + "zod": "^3.20.2", + "debug": "^4.3.4" } } diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index 324692d48bf..fc78dbe67a9 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -1,3 +1,35 @@ +import { getTriggerRun } from "@trigger.dev/sdk/index"; +import { z } from "zod"; import * as events from "./events"; - +import { schemas } from "./internal"; export { events }; + +export type SendTemplateMessageOptions = z.infer< + typeof schemas.messages.SendTemplateMessageBodySchema +>; + +export type SendTemplateMessageResponse = z.infer< + typeof schemas.messages.SendTemplateMessageResponseSchema +>; + +export async function sendTemplate( + key: string, + message: SendTemplateMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendTemplate outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendTemplate", + params: message, + response: { + schema: schemas.messages.SendTemplateMessageResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/whatsapp/src/internal.ts b/integrations/whatsapp/src/internal.ts index c1736765826..6d894e9c95a 100644 --- a/integrations/whatsapp/src/internal.ts +++ b/integrations/whatsapp/src/internal.ts @@ -3,8 +3,10 @@ import type { InternalIntegration, } from "@trigger.dev/integration-sdk"; import { WhatsAppWebhookIntegration } from "./internal/webhooks"; +import { WhatsAppRequestIntegration } from "./internal/requests"; const webhooks = new WhatsAppWebhookIntegration(); +const requests = new WhatsAppRequestIntegration(); const metadata: IntegrationMetadata = { name: "WhatsApp Business Account", @@ -22,6 +24,7 @@ const metadata: IntegrationMetadata = { export const internalIntegration: InternalIntegration = { metadata, webhooks, + requests, }; export * as schemas from "./schemas"; diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts new file mode 100644 index 00000000000..d0f4e46c548 --- /dev/null +++ b/integrations/whatsapp/src/internal/requests.ts @@ -0,0 +1,178 @@ +import { HttpEndpoint, HttpService } from "@trigger.dev/integration-sdk"; +import type { + DisplayProperties, + CacheService, + PerformedRequestResponse, + PerformRequestOptions, + RequestIntegration, + AccessInfo, +} from "@trigger.dev/integration-sdk"; +import debug from "debug"; +import { getAccessToken } from "@trigger.dev/integration-sdk"; +import { z } from "zod"; +import { + SendTemplateMessageBodySchema, + SendTemplateMessageRequestBodySchema, + SendTemplateMessageResponseSchema, +} from "../schemas/messages"; + +const log = debug("trigger:integrations:slack"); + +type SendTemplateMessageRequestBody = z.infer< + typeof SendTemplateMessageRequestBodySchema +>; + +export class WhatsAppRequestIntegration implements RequestIntegration { + #sendTemplateMessageEndpoint = new HttpEndpoint< + typeof SendTemplateMessageResponseSchema, + typeof SendTemplateMessageRequestBodySchema + >({ + response: SendTemplateMessageResponseSchema, + method: "POST", + path: "/messages", + }); + + constructor(private readonly baseUrl: string = "https://slack.com/api") {} + + perform(options: PerformRequestOptions): Promise { + switch (options.endpoint) { + case "message.sendTemplate": { + return this.#sendTemplateMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + default: { + throw new Error(`Unknown endpoint: ${options.endpoint}`); + } + } + } + + displayProperties(endpoint: string, params: any): DisplayProperties { + switch (endpoint) { + case "message.sendTemplate": { + return { + title: `Send template message`, + properties: [], + }; + } + default: { + throw new Error(`Unknown endpoint: ${endpoint}`); + } + } + } + + async #sendTemplateMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendTemplateMessageBodySchema.parse(params); + + log("message.sendTemplate %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const components: SendTemplateMessageRequestBody["template"]["components"] = + []; + + if (parsedParams.parameters?.header) { + components.push({ + type: "header", + parameters: parsedParams.parameters.header, + }); + } + + if (parsedParams.parameters?.body) { + components.push({ + type: "body", + parameters: parsedParams.parameters.body, + }); + } + + if (parsedParams.parameters?.buttons) { + parsedParams.parameters.buttons.forEach((button, index) => { + components.push({ + type: "button", + sub_type: button.sub_type, + index: index, + parameters: button.parameters, + }); + }); + } + + const request: SendTemplateMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "template", + template: { + name: parsedParams.template, + language: { + policy: "deterministic", + code: parsedParams.languageCode, + }, + components, + }, + }; + + const response = await service.performRequest( + this.#sendTemplateMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendTemplate failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = response.statusCode === 200; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendTemplate performedRequest %O", performedRequest); + + return performedRequest; + } + + #isRetryable(statusCode: number): boolean { + return ( + statusCode === 408 || + statusCode === 429 || + statusCode === 500 || + statusCode === 502 || + statusCode === 503 || + statusCode === 504 + ); + } +} diff --git a/integrations/whatsapp/src/schemas/index.ts b/integrations/whatsapp/src/schemas/index.ts index 94516251b26..eadce3bd322 100644 --- a/integrations/whatsapp/src/schemas/index.ts +++ b/integrations/whatsapp/src/schemas/index.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import * as messageEvents from "./messageEvents"; +import * as messages from "./messages"; export const WebhookSourceSchema = z.object({ subresource: z.literal("messages"), @@ -10,4 +11,4 @@ export const WebhookSourceSchema = z.object({ }), }); -export { messageEvents }; +export { messageEvents, messages }; diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts new file mode 100644 index 00000000000..b3a2709abd2 --- /dev/null +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -0,0 +1,134 @@ +import { z } from "zod"; + +const TextParameter = z.object({ + type: z.literal("text"), + text: z.string(), +}); + +const CurrencyParameter = z.object({ + type: z.literal("currency"), + currency: z.object({ + amount_1000: z.number(), + code: z.string(), + fallback_value: z.string(), + }), +}); + +const DateTimeParameter = z.object({ + type: z.literal("date_time"), + date_time: z.object({ + fallback_value: z.string(), + }), +}); + +const ImageParameter = z.object({ + type: z.literal("image"), + image: z.object({ + link: z.string(), + caption: z.string().optional(), + }), +}); + +const VideoParameter = z.object({ + type: z.literal("video"), + video: z.object({ + link: z.string(), + caption: z.string().optional(), + }), +}); + +const DocumentParameter = z.object({ + type: z.literal("document"), + video: z.object({ + link: z.string(), + caption: z.string().optional(), + }), +}); + +const ButtonTemplateComponent = z.object({ + type: z.literal("button"), + sub_type: z.union([z.literal("quick_reply"), z.literal("call_to_action")]), + index: z.number(), + parameters: z.array( + z.discriminatedUnion("type", [ + z.object({ + type: z.literal("payload"), + payload: z.string(), + }), + z.object({ + type: z.literal("text"), + text: z.string(), + }), + ]) + ), +}); + +const HeaderParameters = z + .discriminatedUnion("type", [ + TextParameter, + ImageParameter, + VideoParameter, + DocumentParameter, + ]) + .array(); + +const HeaderTemplateComponent = z.object({ + type: z.literal("header"), + parameters: HeaderParameters, +}); + +const BodyParameters = z.array( + z.discriminatedUnion("type", [ + TextParameter, + CurrencyParameter, + DateTimeParameter, + ]) +); + +const BodyTemplateComponent = z.object({ + type: z.literal("body"), + parameters: BodyParameters, +}); + +export const SendTemplateMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("template"), + template: z.object({ + name: z.string(), + language: z.object({ + code: z.string(), + policy: z.literal("deterministic"), + }), + components: z + .array( + z.discriminatedUnion("type", [ + HeaderTemplateComponent, + BodyTemplateComponent, + ButtonTemplateComponent, + ]) + ) + .optional(), + }), +}); + +export const SendTemplateMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + template: z.string(), + languageCode: z.string(), + parameters: z + .object({ + header: HeaderParameters.optional(), + body: BodyParameters.optional(), + buttons: z.array(ButtonTemplateComponent.omit({ type: true })).optional(), + }) + .optional(), +}); + +export const SendTemplateMessageResponseSchema = z.object({ + messaging_product: z.literal("whatsapp"), + contacts: z.array(z.object({ input: z.string(), wa_id: z.string() })), + messages: z.array(z.object({ id: z.string() })), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5e31490636..3556b2e1c20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -691,18 +691,22 @@ importers: '@trigger.dev/integration-sdk': workspace:* '@trigger.dev/sdk': workspace:* '@trigger.dev/tsconfig': workspace:* + '@types/debug': ^4.1.7 '@types/node': '16' + debug: ^4.3.4 rimraf: ^3.0.2 tsup: ^6.5.0 ulid: ^2.3.0 zod: ^3.20.2 dependencies: + debug: 4.3.4 ulid: 2.3.0 zod: 3.20.2 devDependencies: '@trigger.dev/integration-sdk': link:../../packages/integration-sdk '@trigger.dev/sdk': link:../../packages/trigger-sdk '@trigger.dev/tsconfig': link:../../config-packages/tsconfig + '@types/debug': 4.1.7 '@types/node': 16.18.11 rimraf: 3.0.2 tsup: 6.5.0 From b3bc70baa3dfd1cbed6d343cac3e13e9a312f204 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 10:44:25 +0000 Subject: [PATCH 50/71] Instead of switch statement, grab the integrations object --- .../performIntegrationRequest.server.ts | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index 1bc56205f47..4c9f5e006ec 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -13,6 +13,7 @@ import { prisma } from "~/db.server"; import type { IntegrationRequest } from "~/models/integrationRequest.server"; import { getAccessInfo } from "../accessInfo.server"; import { RedisCacheService } from "../cacheService.server"; +import { getIntegrations } from "~/models/integrations.server"; type CallResponse = | { @@ -227,37 +228,26 @@ export class PerformIntegrationRequest { integrationRequest: IntegrationRequest, cache: CacheService ): Promise { - switch (service) { - case "slack": { - return slack.internalIntegration.requests!.perform({ - accessInfo, - endpoint: integrationRequest.endpoint, - params: integrationRequest.params, - cache, - metadata: { requestId: integrationRequest.id }, - }); - } - case "shopify": { - return shopify.internalIntegration.requests!.perform({ - accessInfo, - endpoint: integrationRequest.endpoint, - params: integrationRequest.params, - cache, - metadata: { requestId: integrationRequest.id }, - }); - } - case "resend": { - return resend.internalIntegration.requests!.perform({ - accessInfo, - endpoint: integrationRequest.endpoint, - params: integrationRequest.params, - cache, - metadata: { requestId: integrationRequest.id }, - }); - } - default: { - throw new Error(`Unknown service: ${service}`); - } + const integrationInfo = getIntegrations(true).find( + (i) => i.metadata.slug === service + ); + + if (!integrationInfo) { + throw new Error(`Unknown service: ${service}`); + } + + const { requests } = integrationInfo; + + if (!requests) { + throw new Error(`Service ${service} does not support requests`); } + + return requests.perform({ + accessInfo, + endpoint: integrationRequest.endpoint, + params: integrationRequest.params, + cache, + metadata: { requestId: integrationRequest.id }, + }); } } From 421bd9f757a8d7bad0307df0d86f61b9a04a8f62 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 31 Jan 2023 12:39:14 +0000 Subject: [PATCH 51/71] Fixed broken integration creation route --- apps/webapp/app/routes/resources/connection/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/resources/connection/index.tsx b/apps/webapp/app/routes/resources/connection/index.tsx index 01f3b73f035..249d961a4da 100644 --- a/apps/webapp/app/routes/resources/connection/index.tsx +++ b/apps/webapp/app/routes/resources/connection/index.tsx @@ -11,7 +11,7 @@ import { } from "~/models/apiConnection.server"; import { connectExternalService } from "~/models/externalService.server"; import { connectExternalSource } from "~/models/externalSource.server"; -import { getIntegrations } from "~/models/integrations.server"; +import { getIntegrationMetadatas } from "~/models/integrations.server"; import { taskQueue } from "~/services/messageBroker.server"; import { requireUserId } from "~/services/session.server"; @@ -91,7 +91,7 @@ export const action = async ({ request }: ActionArgs) => { } const parsed = parsedResult.data; - const integrationInfo = getIntegrations(true).find( + const integrationInfo = getIntegrationMetadatas(true).find( (i) => i.slug === parsed.service ); if (!integrationInfo) { From 8124930841a48a530b1867c974f351707a06017f Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 31 Jan 2023 12:42:49 +0000 Subject: [PATCH 52/71] Fixed call to getIntegrations in new workflow route --- .../__app/orgs/$organizationSlug/__org/workflows.new.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/workflows.new.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/workflows.new.tsx index 5b6e2de84f2..783ed136ebf 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/workflows.new.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/workflows.new.tsx @@ -4,13 +4,13 @@ import { CreateNewWorkflowNoWorkflows } from "~/components/CreateNewWorkflow"; import { Container } from "~/components/layout/Container"; import { SubTitle } from "~/components/primitives/text/SubTitle"; import { Title } from "~/components/primitives/text/Title"; -import { getIntegrations } from "~/models/integrations.server"; +import { getIntegrationMetadatas } from "~/models/integrations.server"; import { requireUserId } from "~/services/session.server"; export const loader = async ({ request, params }: LoaderArgs) => { await requireUserId(request); - const providers = getIntegrations(false); + const providers = getIntegrationMetadatas(false); return typedjson({ providers }); }; From e6d1ddc5d89c587ff575ecd50430040f6971f74b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 31 Jan 2023 13:55:49 +0000 Subject: [PATCH 53/71] Remove exports and add main and types to package.json --- .changeset/fluffy-dragons-mate.md | 8 ++++++++ integrations/github/package.json | 15 ++------------- integrations/resend/package.json | 15 ++------------- integrations/shopify/package.json | 15 ++------------- integrations/slack/package.json | 15 ++------------- 5 files changed, 16 insertions(+), 52 deletions(-) create mode 100644 .changeset/fluffy-dragons-mate.md diff --git a/.changeset/fluffy-dragons-mate.md b/.changeset/fluffy-dragons-mate.md new file mode 100644 index 00000000000..692e2d3456d --- /dev/null +++ b/.changeset/fluffy-dragons-mate.md @@ -0,0 +1,8 @@ +--- +"@trigger.dev/shopify": patch +"@trigger.dev/github": patch +"@trigger.dev/resend": patch +"@trigger.dev/slack": patch +--- + +Remove exports and add main and types to package.json diff --git a/integrations/github/package.json b/integrations/github/package.json index 1366fc26aa9..f78d22be9a1 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -2,19 +2,8 @@ "name": "@trigger.dev/github", "version": "0.1.17", "description": "The official GitHub integration for Trigger.dev", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./internal": { - "import": "./dist/internal.js", - "require": "./dist/internal.js", - "types": "./dist/internal.d.ts" - }, - "./package.json": "./package.json" - }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "publishConfig": { "access": "public" }, diff --git a/integrations/resend/package.json b/integrations/resend/package.json index f9bca8a39b4..026e86a62ce 100644 --- a/integrations/resend/package.json +++ b/integrations/resend/package.json @@ -2,19 +2,8 @@ "name": "@trigger.dev/resend", "version": "0.1.17", "description": "The official resend.com integration for Trigger.dev", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./internal": { - "import": "./dist/internal.js", - "require": "./dist/internal.js", - "types": "./dist/internal.d.ts" - }, - "./package.json": "./package.json" - }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "publishConfig": { "access": "public" }, diff --git a/integrations/shopify/package.json b/integrations/shopify/package.json index c50142a50cf..217b8a480f8 100644 --- a/integrations/shopify/package.json +++ b/integrations/shopify/package.json @@ -2,19 +2,8 @@ "name": "@trigger.dev/shopify", "version": "0.1.17", "description": "The official Shopify integration for Trigger.dev", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./internal": { - "import": "./dist/internal.js", - "require": "./dist/internal.js", - "types": "./dist/internal.d.ts" - }, - "./package.json": "./package.json" - }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "publishConfig": { "access": "public" }, diff --git a/integrations/slack/package.json b/integrations/slack/package.json index 86566e21cf6..6e38696f263 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -2,19 +2,8 @@ "name": "@trigger.dev/slack", "version": "0.1.17", "description": "The official Slack integration for Trigger.dev", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./internal": { - "import": "./dist/internal.js", - "require": "./dist/internal.js", - "types": "./dist/internal.d.ts" - }, - "./package.json": "./package.json" - }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "publishConfig": { "access": "public" }, From e6ce7ea8599af47ea8c8f80c898d16d4380fea8b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 11:00:43 +0000 Subject: [PATCH 54/71] WhatsApp basic template message is sending --- integrations/whatsapp/src/internal/requests.ts | 4 +++- integrations/whatsapp/src/schemas/messages.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index d0f4e46c548..69e4d3a5ee3 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -32,7 +32,9 @@ export class WhatsAppRequestIntegration implements RequestIntegration { path: "/messages", }); - constructor(private readonly baseUrl: string = "https://slack.com/api") {} + constructor( + private readonly baseUrl: string = "https://graph.facebook.com/v15.0" + ) {} perform(options: PerformRequestOptions): Promise { switch (options.endpoint) { diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index b3a2709abd2..b291bc56d35 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -122,7 +122,9 @@ export const SendTemplateMessageBodySchema = z.object({ .object({ header: HeaderParameters.optional(), body: BodyParameters.optional(), - buttons: z.array(ButtonTemplateComponent.omit({ type: true })).optional(), + buttons: z + .array(ButtonTemplateComponent.omit({ type: true, index: true })) + .optional(), }) .optional(), }); From 868e5f4ed16a8bf00611c2f16414a1bcb547c856 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 11:00:54 +0000 Subject: [PATCH 55/71] Changed the WhatsApp package exports to match the other integrations --- integrations/whatsapp/package.json | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/integrations/whatsapp/package.json b/integrations/whatsapp/package.json index b10421b9809..bbed470c5c4 100644 --- a/integrations/whatsapp/package.json +++ b/integrations/whatsapp/package.json @@ -2,19 +2,8 @@ "name": "@trigger.dev/whatsapp", "version": "0.1.16", "description": "The official WhatsApp Business integration for Trigger.dev", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./internal": { - "import": "./dist/internal.js", - "require": "./dist/internal.js", - "types": "./dist/internal.d.ts" - }, - "./package.json": "./package.json" - }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "publishConfig": { "access": "public" }, From afc47d328f9907d72d1f8455ac810b6184674c0d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 11:31:06 +0000 Subject: [PATCH 56/71] =?UTF-8?q?Send=20=E2=80=9Ctext=E2=80=9D=20WhatsApp?= =?UTF-8?q?=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/whatsapp/src/index.ts | 10 +- integrations/whatsapp/src/index.ts | 34 +++++- .../whatsapp/src/internal/requests.ts | 100 +++++++++++++++++- integrations/whatsapp/src/schemas/messages.ts | 20 +++- 4 files changed, 155 insertions(+), 9 deletions(-) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index e611bafb74c..8f2c2165db2 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -1,5 +1,5 @@ import { Trigger } from "@trigger.dev/sdk"; -import { events, sendTemplate } from "@trigger.dev/whatsapp"; +import { events, sendTemplate, sendText } from "@trigger.dev/whatsapp"; new Trigger({ id: "whatsapp-webhook", @@ -14,11 +14,17 @@ new Trigger({ await ctx.logger.info(`Message data`, event.message); await ctx.logger.info(`Phone number`, event.contacts[0]); - const response = await sendTemplate("template-msg", { + const templateResponse = await sendTemplate("template-msg", { fromId: event.metadata.phone_number_id, to: event.message.from, template: "hello_world", languageCode: "en_US", }); + + const textResponse = await sendText("text-msg", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + text: "Hello! This is a text sent automatically from https://www.trigger.dev", + }); }, }).listen(); diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index fc78dbe67a9..d1527857680 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -9,7 +9,7 @@ export type SendTemplateMessageOptions = z.infer< >; export type SendTemplateMessageResponse = z.infer< - typeof schemas.messages.SendTemplateMessageResponseSchema + typeof schemas.messages.SendMessageResponseSchema >; export async function sendTemplate( @@ -27,7 +27,37 @@ export async function sendTemplate( endpoint: "message.sendTemplate", params: message, response: { - schema: schemas.messages.SendTemplateMessageResponseSchema, + schema: schemas.messages.SendMessageResponseSchema, + }, + }); + + return output; +} + +export type SendTextMessageOptions = z.infer< + typeof schemas.messages.SendTextMessageBodySchema +>; + +export type SendTextMessageResponse = z.infer< + typeof schemas.messages.SendMessageResponseSchema +>; + +export async function sendText( + key: string, + message: SendTextMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendText outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendText", + params: message, + response: { + schema: schemas.messages.SendMessageResponseSchema, }, }); diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index 69e4d3a5ee3..740fafca133 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -13,21 +13,36 @@ import { z } from "zod"; import { SendTemplateMessageBodySchema, SendTemplateMessageRequestBodySchema, - SendTemplateMessageResponseSchema, + SendMessageResponseSchema, + SendTextMessageBodySchema, + SendTextMessageRequestBodySchema, } from "../schemas/messages"; -const log = debug("trigger:integrations:slack"); +const log = debug("trigger:integrations:whatsapp"); type SendTemplateMessageRequestBody = z.infer< typeof SendTemplateMessageRequestBodySchema >; +type SendTextMessageRequestBody = z.infer< + typeof SendTextMessageRequestBodySchema +>; + export class WhatsAppRequestIntegration implements RequestIntegration { #sendTemplateMessageEndpoint = new HttpEndpoint< - typeof SendTemplateMessageResponseSchema, + typeof SendMessageResponseSchema, typeof SendTemplateMessageRequestBodySchema >({ - response: SendTemplateMessageResponseSchema, + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + + #sendTextMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendTextMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, method: "POST", path: "/messages", }); @@ -46,6 +61,14 @@ export class WhatsAppRequestIntegration implements RequestIntegration { options.metadata ); } + case "message.sendText": { + return this.#sendTextMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } default: { throw new Error(`Unknown endpoint: ${options.endpoint}`); } @@ -167,6 +190,75 @@ export class WhatsAppRequestIntegration implements RequestIntegration { return performedRequest; } + async #sendTextMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendTextMessageBodySchema.parse(params); + + log("message.sendText %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendTextMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "text", + text: { + body: parsedParams.text, + preview_url: parsedParams.preview_url ?? true, + }, + }; + + const response = await service.performRequest( + this.#sendTextMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendText failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = response.statusCode === 200; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendText performedRequest %O", performedRequest); + + return performedRequest; + } + #isRetryable(statusCode: number): boolean { return ( statusCode === 408 || diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index b291bc56d35..e28c687766b 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -129,8 +129,26 @@ export const SendTemplateMessageBodySchema = z.object({ .optional(), }); -export const SendTemplateMessageResponseSchema = z.object({ +export const SendMessageResponseSchema = z.object({ messaging_product: z.literal("whatsapp"), contacts: z.array(z.object({ input: z.string(), wa_id: z.string() })), messages: z.array(z.object({ id: z.string() })), }); + +export const SendTextMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("text"), + text: z.object({ + body: z.string(), + preview_url: z.boolean(), + }), +}); + +export const SendTextMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + text: z.string(), + preview_url: z.boolean().optional(), +}); From 405457192960dc6d42e4b8aa5eccfa3e5e6dcbd9 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 11:34:13 +0000 Subject: [PATCH 57/71] Metadata required for the run page --- integrations/whatsapp/src/internal/requests.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index 740fafca133..f06631b70f6 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -78,8 +78,16 @@ export class WhatsAppRequestIntegration implements RequestIntegration { displayProperties(endpoint: string, params: any): DisplayProperties { switch (endpoint) { case "message.sendTemplate": { + const parsedParams = SendTemplateMessageBodySchema.parse(params); return { - title: `Send template message`, + title: `Send template (${parsedParams.template}) to ${parsedParams.to}`, + properties: [], + }; + } + case "message.sendText": { + const parsedParams = SendTextMessageBodySchema.parse(params); + return { + title: `Send text to ${parsedParams.to}`, properties: [], }; } From d57339a7c1a4e41bb2c895a5f5e950c677c49995 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 11:48:48 +0000 Subject: [PATCH 58/71] Reactions and replying is working --- examples/whatsapp/src/index.ts | 21 +++- integrations/whatsapp/src/index.ts | 30 ++++++ .../whatsapp/src/internal/requests.ts | 102 ++++++++++++++++++ integrations/whatsapp/src/schemas/messages.ts | 26 +++++ 4 files changed, 178 insertions(+), 1 deletion(-) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 8f2c2165db2..5e4fe13d55c 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -1,5 +1,10 @@ import { Trigger } from "@trigger.dev/sdk"; -import { events, sendTemplate, sendText } from "@trigger.dev/whatsapp"; +import { + events, + sendReaction, + sendTemplate, + sendText, +} from "@trigger.dev/whatsapp"; new Trigger({ id: "whatsapp-webhook", @@ -14,6 +19,13 @@ new Trigger({ await ctx.logger.info(`Message data`, event.message); await ctx.logger.info(`Phone number`, event.contacts[0]); + const reactionResponse = await sendReaction("reaction", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + isReplyTo: event.message.id, + emoji: "🥰", + }); + const templateResponse = await sendTemplate("template-msg", { fromId: event.metadata.phone_number_id, to: event.message.from, @@ -26,5 +38,12 @@ new Trigger({ to: event.message.from, text: "Hello! This is a text sent automatically from https://www.trigger.dev", }); + + const replyResponse = await sendText("reply-text-msg", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + text: "Hi, this is a reply to the automated message that was just sent", + isReplyTo: textResponse.messages[0].id, + }); }, }).listen(); diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index d1527857680..4a924f79b56 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -63,3 +63,33 @@ export async function sendText( return output; } + +export type SendReactionMessageOptions = z.infer< + typeof schemas.messages.SendReactionMessageBodySchema +>; + +export type SendReactionMessageResponse = z.infer< + typeof schemas.messages.SendMessageResponseSchema +>; + +export async function sendReaction( + key: string, + message: SendReactionMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendReaction outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendReaction", + params: message, + response: { + schema: schemas.messages.SendMessageResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index f06631b70f6..db411d7f120 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -16,6 +16,8 @@ import { SendMessageResponseSchema, SendTextMessageBodySchema, SendTextMessageRequestBodySchema, + SendReactionMessageBodySchema, + SendReactionMessageRequestBodySchema, } from "../schemas/messages"; const log = debug("trigger:integrations:whatsapp"); @@ -28,6 +30,10 @@ type SendTextMessageRequestBody = z.infer< typeof SendTextMessageRequestBodySchema >; +type SendReactionMessageRequestBody = z.infer< + typeof SendReactionMessageRequestBodySchema +>; + export class WhatsAppRequestIntegration implements RequestIntegration { #sendTemplateMessageEndpoint = new HttpEndpoint< typeof SendMessageResponseSchema, @@ -47,6 +53,15 @@ export class WhatsAppRequestIntegration implements RequestIntegration { path: "/messages", }); + #sendReactionMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendReactionMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + constructor( private readonly baseUrl: string = "https://graph.facebook.com/v15.0" ) {} @@ -69,6 +84,14 @@ export class WhatsAppRequestIntegration implements RequestIntegration { options.metadata ); } + case "message.sendReaction": { + return this.#sendReactionMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } default: { throw new Error(`Unknown endpoint: ${options.endpoint}`); } @@ -91,6 +114,13 @@ export class WhatsAppRequestIntegration implements RequestIntegration { properties: [], }; } + case "message.sendReaction": { + const parsedParams = SendReactionMessageBodySchema.parse(params); + return { + title: `Send ${parsedParams.emoji} reaction to ${parsedParams.to}`, + properties: [], + }; + } default: { throw new Error(`Unknown endpoint: ${endpoint}`); } @@ -225,6 +255,9 @@ export class WhatsAppRequestIntegration implements RequestIntegration { body: parsedParams.text, preview_url: parsedParams.preview_url ?? true, }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, }; const response = await service.performRequest( @@ -267,6 +300,75 @@ export class WhatsAppRequestIntegration implements RequestIntegration { return performedRequest; } + async #sendReactionMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendReactionMessageBodySchema.parse(params); + + log("message.sendReaction %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendReactionMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "reaction", + reaction: { + message_id: parsedParams.isReplyTo, + emoji: parsedParams.emoji, + }, + }; + + const response = await service.performRequest( + this.#sendReactionMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendReaction failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = response.statusCode === 200; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendReaction performedRequest %O", performedRequest); + + return performedRequest; + } + #isRetryable(statusCode: number): boolean { return ( statusCode === 408 || diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index e28c687766b..b3b7f466a5d 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -135,6 +135,12 @@ export const SendMessageResponseSchema = z.object({ messages: z.array(z.object({ id: z.string() })), }); +const MessageContextSchema = z + .object({ + message_id: z.string(), + }) + .optional(); + export const SendTextMessageRequestBodySchema = z.object({ messaging_product: z.literal("whatsapp"), recipient_type: z.literal("individual"), @@ -144,6 +150,7 @@ export const SendTextMessageRequestBodySchema = z.object({ body: z.string(), preview_url: z.boolean(), }), + context: MessageContextSchema, }); export const SendTextMessageBodySchema = z.object({ @@ -151,4 +158,23 @@ export const SendTextMessageBodySchema = z.object({ to: z.string(), text: z.string(), preview_url: z.boolean().optional(), + isReplyTo: z.string().optional(), +}); + +export const SendReactionMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("reaction"), + reaction: z.object({ + message_id: z.string(), + emoji: z.string(), + }), +}); + +export const SendReactionMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + isReplyTo: z.string(), + emoji: z.string(), }); From c1742f0d1b35697b97bf9d8e57ae58b2f833ddee Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 11:59:59 +0000 Subject: [PATCH 59/71] Sending images working --- examples/whatsapp/src/index.ts | 8 ++ integrations/whatsapp/src/index.ts | 30 ++++++ .../whatsapp/src/internal/requests.ts | 102 ++++++++++++++++++ integrations/whatsapp/src/schemas/messages.ts | 37 ++++--- 4 files changed, 165 insertions(+), 12 deletions(-) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 5e4fe13d55c..0838c43df89 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -1,6 +1,7 @@ import { Trigger } from "@trigger.dev/sdk"; import { events, + sendImage, sendReaction, sendTemplate, sendText, @@ -45,5 +46,12 @@ new Trigger({ text: "Hi, this is a reply to the automated message that was just sent", isReplyTo: textResponse.messages[0].id, }); + + const imageResponse = await sendImage("image", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://app.trigger.dev/emails/logo.png", + caption: "This is a genius caption", + }); }, }).listen(); diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index 4a924f79b56..933128ec7e6 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -93,3 +93,33 @@ export async function sendReaction( return output; } + +export type SendImageMessageOptions = z.infer< + typeof schemas.messages.SendImageMessageBodySchema +>; + +export type SendImageMessageResponse = z.infer< + typeof schemas.messages.SendMessageResponseSchema +>; + +export async function sendImage( + key: string, + message: SendImageMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendImage outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendImage", + params: message, + response: { + schema: schemas.messages.SendMessageResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index db411d7f120..80849fc59d1 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -18,6 +18,8 @@ import { SendTextMessageRequestBodySchema, SendReactionMessageBodySchema, SendReactionMessageRequestBodySchema, + SendImageMessageBodySchema, + SendImageMessageRequestBodySchema, } from "../schemas/messages"; const log = debug("trigger:integrations:whatsapp"); @@ -34,6 +36,10 @@ type SendReactionMessageRequestBody = z.infer< typeof SendReactionMessageRequestBodySchema >; +type SendImageMessageRequestBody = z.infer< + typeof SendImageMessageRequestBodySchema +>; + export class WhatsAppRequestIntegration implements RequestIntegration { #sendTemplateMessageEndpoint = new HttpEndpoint< typeof SendMessageResponseSchema, @@ -62,6 +68,15 @@ export class WhatsAppRequestIntegration implements RequestIntegration { path: "/messages", }); + #sendImageMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendImageMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + constructor( private readonly baseUrl: string = "https://graph.facebook.com/v15.0" ) {} @@ -92,6 +107,14 @@ export class WhatsAppRequestIntegration implements RequestIntegration { options.metadata ); } + case "message.sendImage": { + return this.#sendImageMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } default: { throw new Error(`Unknown endpoint: ${options.endpoint}`); } @@ -121,6 +144,13 @@ export class WhatsAppRequestIntegration implements RequestIntegration { properties: [], }; } + case "message.sendImage": { + const parsedParams = SendImageMessageBodySchema.parse(params); + return { + title: `Send image (${parsedParams.url}) to ${parsedParams.to}`, + properties: [], + }; + } default: { throw new Error(`Unknown endpoint: ${endpoint}`); } @@ -369,6 +399,78 @@ export class WhatsAppRequestIntegration implements RequestIntegration { return performedRequest; } + async #sendImageMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendImageMessageBodySchema.parse(params); + + log("message.sendImage %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendImageMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "image", + image: { + link: parsedParams.url, + caption: parsedParams.caption, + }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendImageMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendImage failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = response.statusCode === 200; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendImage performedRequest %O", performedRequest); + + return performedRequest; + } + #isRetryable(statusCode: number): boolean { return ( statusCode === 408 || diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index b3b7f466a5d..39c6d76f63a 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -21,28 +21,24 @@ const DateTimeParameter = z.object({ }), }); +const MediaObject = z.object({ + link: z.string(), + caption: z.string().optional(), +}); + const ImageParameter = z.object({ type: z.literal("image"), - image: z.object({ - link: z.string(), - caption: z.string().optional(), - }), + image: MediaObject, }); const VideoParameter = z.object({ type: z.literal("video"), - video: z.object({ - link: z.string(), - caption: z.string().optional(), - }), + video: MediaObject, }); const DocumentParameter = z.object({ type: z.literal("document"), - video: z.object({ - link: z.string(), - caption: z.string().optional(), - }), + video: MediaObject, }); const ButtonTemplateComponent = z.object({ @@ -178,3 +174,20 @@ export const SendReactionMessageBodySchema = z.object({ isReplyTo: z.string(), emoji: z.string(), }); + +export const SendImageMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("image"), + image: MediaObject, + context: MessageContextSchema, +}); + +export const SendImageMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + url: z.string(), + caption: z.string().optional(), + isReplyTo: z.string().optional(), +}); From 731b6308e00d4b964ed44aa599f07d4cb1222678 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 12:14:32 +0000 Subject: [PATCH 60/71] Sending locations is working --- examples/whatsapp/src/index.ts | 10 ++ integrations/whatsapp/src/index.ts | 30 +++++ .../whatsapp/src/internal/requests.ts | 104 ++++++++++++++++++ integrations/whatsapp/src/schemas/messages.ts | 26 +++++ 4 files changed, 170 insertions(+) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 0838c43df89..5b2383d2f96 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -2,6 +2,7 @@ import { Trigger } from "@trigger.dev/sdk"; import { events, sendImage, + sendLocation, sendReaction, sendTemplate, sendText, @@ -53,5 +54,14 @@ new Trigger({ url: "https://app.trigger.dev/emails/logo.png", caption: "This is a genius caption", }); + + const locationResponse = await sendLocation("location", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + latitude: 37.422, + longitude: -122.084, + name: "Trigger.dev HQ", + address: "123 Main St, San Francisco, CA 94105", + }); }, }).listen(); diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index 933128ec7e6..9fc370d4a09 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -123,3 +123,33 @@ export async function sendImage( return output; } + +export type SendLocationMessageOptions = z.infer< + typeof schemas.messages.SendLocationMessageBodySchema +>; + +export type SendLocationMessageResponse = z.infer< + typeof schemas.messages.SendMessageResponseSchema +>; + +export async function sendLocation( + key: string, + message: SendLocationMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendLocation outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendLocation", + params: message, + response: { + schema: schemas.messages.SendMessageResponseSchema, + }, + }); + + return output; +} diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index 80849fc59d1..33e6e1a7216 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -20,6 +20,8 @@ import { SendReactionMessageRequestBodySchema, SendImageMessageBodySchema, SendImageMessageRequestBodySchema, + SendLocationMessageRequestBodySchema, + SendLocationMessageBodySchema, } from "../schemas/messages"; const log = debug("trigger:integrations:whatsapp"); @@ -40,6 +42,10 @@ type SendImageMessageRequestBody = z.infer< typeof SendImageMessageRequestBodySchema >; +type SendLocationMessageRequestBody = z.infer< + typeof SendLocationMessageRequestBodySchema +>; + export class WhatsAppRequestIntegration implements RequestIntegration { #sendTemplateMessageEndpoint = new HttpEndpoint< typeof SendMessageResponseSchema, @@ -77,6 +83,15 @@ export class WhatsAppRequestIntegration implements RequestIntegration { path: "/messages", }); + #sendLocationMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendLocationMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + constructor( private readonly baseUrl: string = "https://graph.facebook.com/v15.0" ) {} @@ -115,6 +130,14 @@ export class WhatsAppRequestIntegration implements RequestIntegration { options.metadata ); } + case "message.sendLocation": { + return this.#sendLocationMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } default: { throw new Error(`Unknown endpoint: ${options.endpoint}`); } @@ -151,6 +174,13 @@ export class WhatsAppRequestIntegration implements RequestIntegration { properties: [], }; } + case "message.sendLocation": { + const parsedParams = SendLocationMessageBodySchema.parse(params); + return { + title: `Send location to ${parsedParams.to}`, + properties: [], + }; + } default: { throw new Error(`Unknown endpoint: ${endpoint}`); } @@ -471,6 +501,80 @@ export class WhatsAppRequestIntegration implements RequestIntegration { return performedRequest; } + async #sendLocationMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendLocationMessageBodySchema.parse(params); + + log("message.sendLocation %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendLocationMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "location", + location: { + latitude: parsedParams.latitude, + longitude: parsedParams.longitude, + name: parsedParams.name, + address: parsedParams.address, + }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendLocationMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendLocation failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = response.statusCode === 200; + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendLocation performedRequest %O", performedRequest); + + return performedRequest; + } + #isRetryable(statusCode: number): boolean { return ( statusCode === 408 || diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index 39c6d76f63a..3cf9f2ae67b 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -59,6 +59,13 @@ const ButtonTemplateComponent = z.object({ ), }); +const LocationObject = z.object({ + latitude: z.number(), + longitude: z.number(), + name: z.string().optional(), + address: z.string().optional(), +}); + const HeaderParameters = z .discriminatedUnion("type", [ TextParameter, @@ -191,3 +198,22 @@ export const SendImageMessageBodySchema = z.object({ caption: z.string().optional(), isReplyTo: z.string().optional(), }); + +export const SendLocationMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("location"), + location: LocationObject, + context: MessageContextSchema, +}); + +export const SendLocationMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + latitude: z.number(), + longitude: z.number(), + name: z.string().optional(), + address: z.string().optional(), + isReplyTo: z.string().optional(), +}); From 93f1814fd2120b0a8cbf7f2e7ae31fe164f61ef7 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 13:15:15 +0000 Subject: [PATCH 61/71] Improved errors handling, added sendContacts --- examples/whatsapp/src/index.ts | 20 ++++ integrations/whatsapp/src/index.ts | 50 ++++++-- .../whatsapp/src/internal/requests.ts | 112 +++++++++++++++++- .../whatsapp/src/internal/webhooks.ts | 14 --- .../whatsapp/src/schemas/messageEvents.ts | 34 +----- integrations/whatsapp/src/schemas/messages.ts | 40 ++++++- integrations/whatsapp/src/schemas/shared.ts | 54 +++++++++ 7 files changed, 260 insertions(+), 64 deletions(-) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 5b2383d2f96..67c66ecccf9 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -1,6 +1,7 @@ import { Trigger } from "@trigger.dev/sdk"; import { events, + sendContacts, sendImage, sendLocation, sendReaction, @@ -63,5 +64,24 @@ new Trigger({ name: "Trigger.dev HQ", address: "123 Main St, San Francisco, CA 94105", }); + + const contactsResponse = await sendContacts("contacts", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + contacts: [ + { + name: { + first_name: "Matt", + last_name: "Aitken", + formatted_name: "Matt Aitken", + }, + phones: [ + { + phone: "+12345678901", + }, + ], + }, + ], + }); }, }).listen(); diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index 9fc370d4a09..87eb0084fdf 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -9,7 +9,7 @@ export type SendTemplateMessageOptions = z.infer< >; export type SendTemplateMessageResponse = z.infer< - typeof schemas.messages.SendMessageResponseSchema + typeof schemas.messages.SendMessageSuccessResponseSchema >; export async function sendTemplate( @@ -27,7 +27,7 @@ export async function sendTemplate( endpoint: "message.sendTemplate", params: message, response: { - schema: schemas.messages.SendMessageResponseSchema, + schema: schemas.messages.SendMessageSuccessResponseSchema, }, }); @@ -39,7 +39,7 @@ export type SendTextMessageOptions = z.infer< >; export type SendTextMessageResponse = z.infer< - typeof schemas.messages.SendMessageResponseSchema + typeof schemas.messages.SendMessageSuccessResponseSchema >; export async function sendText( @@ -57,7 +57,7 @@ export async function sendText( endpoint: "message.sendText", params: message, response: { - schema: schemas.messages.SendMessageResponseSchema, + schema: schemas.messages.SendMessageSuccessResponseSchema, }, }); @@ -69,7 +69,7 @@ export type SendReactionMessageOptions = z.infer< >; export type SendReactionMessageResponse = z.infer< - typeof schemas.messages.SendMessageResponseSchema + typeof schemas.messages.SendMessageSuccessResponseSchema >; export async function sendReaction( @@ -87,7 +87,7 @@ export async function sendReaction( endpoint: "message.sendReaction", params: message, response: { - schema: schemas.messages.SendMessageResponseSchema, + schema: schemas.messages.SendMessageSuccessResponseSchema, }, }); @@ -99,7 +99,7 @@ export type SendImageMessageOptions = z.infer< >; export type SendImageMessageResponse = z.infer< - typeof schemas.messages.SendMessageResponseSchema + typeof schemas.messages.SendMessageSuccessResponseSchema >; export async function sendImage( @@ -117,7 +117,7 @@ export async function sendImage( endpoint: "message.sendImage", params: message, response: { - schema: schemas.messages.SendMessageResponseSchema, + schema: schemas.messages.SendMessageSuccessResponseSchema, }, }); @@ -129,7 +129,7 @@ export type SendLocationMessageOptions = z.infer< >; export type SendLocationMessageResponse = z.infer< - typeof schemas.messages.SendMessageResponseSchema + typeof schemas.messages.SendMessageSuccessResponseSchema >; export async function sendLocation( @@ -147,7 +147,37 @@ export async function sendLocation( endpoint: "message.sendLocation", params: message, response: { - schema: schemas.messages.SendMessageResponseSchema, + schema: schemas.messages.SendMessageSuccessResponseSchema, + }, + }); + + return output; +} + +export type SendContactsMessageOptions = z.infer< + typeof schemas.messages.SendContactsMessageBodySchema +>; + +export type SendContactsMessageResponse = z.infer< + typeof schemas.messages.SendMessageSuccessResponseSchema +>; + +export async function sendContacts( + key: string, + message: SendContactsMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendContacts outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendContacts", + params: message, + response: { + schema: schemas.messages.SendMessageSuccessResponseSchema, }, }); diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index 33e6e1a7216..3820351bb44 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -13,7 +13,7 @@ import { z } from "zod"; import { SendTemplateMessageBodySchema, SendTemplateMessageRequestBodySchema, - SendMessageResponseSchema, + SendMessageSuccessResponseSchema, SendTextMessageBodySchema, SendTextMessageRequestBodySchema, SendReactionMessageBodySchema, @@ -22,6 +22,9 @@ import { SendImageMessageRequestBodySchema, SendLocationMessageRequestBodySchema, SendLocationMessageBodySchema, + SendContactsMessageRequestBodySchema, + SendContactsMessageBodySchema, + SendMessageResponseSchema, } from "../schemas/messages"; const log = debug("trigger:integrations:whatsapp"); @@ -46,6 +49,10 @@ type SendLocationMessageRequestBody = z.infer< typeof SendLocationMessageRequestBodySchema >; +type SendContactsMessageRequestBody = z.infer< + typeof SendContactsMessageRequestBodySchema +>; + export class WhatsAppRequestIntegration implements RequestIntegration { #sendTemplateMessageEndpoint = new HttpEndpoint< typeof SendMessageResponseSchema, @@ -92,6 +99,15 @@ export class WhatsAppRequestIntegration implements RequestIntegration { path: "/messages", }); + #sendContactsMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendContactsMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + constructor( private readonly baseUrl: string = "https://graph.facebook.com/v15.0" ) {} @@ -138,6 +154,14 @@ export class WhatsAppRequestIntegration implements RequestIntegration { options.metadata ); } + case "message.sendContacts": { + return this.#sendContactsMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } default: { throw new Error(`Unknown endpoint: ${options.endpoint}`); } @@ -181,6 +205,13 @@ export class WhatsAppRequestIntegration implements RequestIntegration { properties: [], }; } + case "message.sendContacts": { + const parsedParams = SendContactsMessageBodySchema.parse(params); + return { + title: `Send contacts to ${parsedParams.to}`, + properties: [], + }; + } default: { throw new Error(`Unknown endpoint: ${endpoint}`); } @@ -269,7 +300,7 @@ export class WhatsAppRequestIntegration implements RequestIntegration { }; } - const ok = response.statusCode === 200; + const ok = !("error" in response.data); const performedRequest = { ok, @@ -341,7 +372,7 @@ export class WhatsAppRequestIntegration implements RequestIntegration { }; } - const ok = response.statusCode === 200; + const ok = !("error" in response.data); const performedRequest = { ok, @@ -410,7 +441,7 @@ export class WhatsAppRequestIntegration implements RequestIntegration { }; } - const ok = response.statusCode === 200; + const ok = !("error" in response.data); const performedRequest = { ok, @@ -482,7 +513,7 @@ export class WhatsAppRequestIntegration implements RequestIntegration { }; } - const ok = response.statusCode === 200; + const ok = !("error" in response.data); const performedRequest = { ok, @@ -556,7 +587,7 @@ export class WhatsAppRequestIntegration implements RequestIntegration { }; } - const ok = response.statusCode === 200; + const ok = !("error" in response.data); const performedRequest = { ok, @@ -575,6 +606,75 @@ export class WhatsAppRequestIntegration implements RequestIntegration { return performedRequest; } + async #sendContactsMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendContactsMessageBodySchema.parse(params); + + log("message.sendContacts %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendContactsMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "contacts", + contacts: parsedParams.contacts, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendContactsMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendContacts failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = !("error" in response.data); + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendContacts performedRequest %O", performedRequest); + + return performedRequest; + } + #isRetryable(statusCode: number): boolean { return ( statusCode === 408 || diff --git a/integrations/whatsapp/src/internal/webhooks.ts b/integrations/whatsapp/src/internal/webhooks.ts index 3ce304844d2..e5c6daec3f3 100644 --- a/integrations/whatsapp/src/internal/webhooks.ts +++ b/integrations/whatsapp/src/internal/webhooks.ts @@ -49,20 +49,6 @@ export class WhatsAppWebhookIntegration implements WebhookIntegration { } handleWebhookRequest(options: HandleWebhookOptions) { - console.log( - "Handling WhatsApp search params", - options.request.searchParams.toString() - ); - console.log( - "Handling WhatsApp headers", - JSON.stringify(options.request.headers) - ); - - console.log( - "Handling WhatsApp webhook request", - JSON.stringify(options.request.body) - ); - const context = omit(options.request.headers, [ "x-hub-signature-256", "x-hub-signature", diff --git a/integrations/whatsapp/src/schemas/messageEvents.ts b/integrations/whatsapp/src/schemas/messageEvents.ts index ebf89b9edc8..aec37d17753 100644 --- a/integrations/whatsapp/src/schemas/messageEvents.ts +++ b/integrations/whatsapp/src/schemas/messageEvents.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { sharedContactSchema } from "./shared"; const metadataSchema = z.object({ display_phone_number: z.string(), @@ -96,39 +97,6 @@ const locationMessageEventSchema = z.object({ }), }); -const sharedContactSchema = z.object({ - name: z - .object({ - last_name: z.string().optional(), - first_name: z.string().optional(), - formatted_name: z.string().optional(), - }) - .optional(), - emails: z - .array(z.object({ type: z.string().optional(), email: z.string() })) - .optional(), - phones: z - .array( - z.object({ - type: z.string().optional(), - phone: z.string(), - wa_id: z.string().optional(), - }) - ) - .optional(), - birthday: z.string().optional(), - addresses: z - .array( - z.object({ - zip: z.string().optional(), - city: z.string().optional(), - type: z.string().optional(), - street: z.string().optional(), - }) - ) - .optional(), -}); - const contactsMessageEventSchema = z.object({ type: z.literal("contacts"), contacts: z.array(sharedContactSchema), diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index 3cf9f2ae67b..d707ce9d035 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { sharedContactSchema } from "./shared"; const TextParameter = z.object({ type: z.literal("text"), @@ -132,12 +133,33 @@ export const SendTemplateMessageBodySchema = z.object({ .optional(), }); -export const SendMessageResponseSchema = z.object({ +export const SendMessageErrorResponseSchema = z + .object({ + error: z.object({ + message: z.string(), + type: z.string(), + code: z.number(), + error_data: z.object({ + messaging_product: z.string(), + details: z.string(), + }), + error_subcode: z.number(), + fbtrace_id: z.string(), + }), + }) + .passthrough(); + +export const SendMessageSuccessResponseSchema = z.object({ messaging_product: z.literal("whatsapp"), contacts: z.array(z.object({ input: z.string(), wa_id: z.string() })), messages: z.array(z.object({ id: z.string() })), }); +export const SendMessageResponseSchema = z.union([ + SendMessageErrorResponseSchema, + SendMessageSuccessResponseSchema, +]); + const MessageContextSchema = z .object({ message_id: z.string(), @@ -217,3 +239,19 @@ export const SendLocationMessageBodySchema = z.object({ address: z.string().optional(), isReplyTo: z.string().optional(), }); + +export const SendContactsMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("contacts"), + contacts: z.array(sharedContactSchema), + context: MessageContextSchema, +}); + +export const SendContactsMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + contacts: z.array(sharedContactSchema), + isReplyTo: z.string().optional(), +}); diff --git a/integrations/whatsapp/src/schemas/shared.ts b/integrations/whatsapp/src/schemas/shared.ts index 9d50a9238cf..0b47b7fc981 100644 --- a/integrations/whatsapp/src/schemas/shared.ts +++ b/integrations/whatsapp/src/schemas/shared.ts @@ -1 +1,55 @@ import { z } from "zod"; + +export const sharedContactSchema = z.object({ + name: z + .object({ + formatted_name: z.string(), + first_name: z.string().optional(), + middle_name: z.string().optional(), + last_name: z.string().optional(), + suffix: z.string().optional(), + prefix: z.string().optional(), + }) + .optional(), + emails: z + .array(z.object({ type: z.string().optional(), email: z.string() })) + .optional(), + phones: z + .array( + z.object({ + type: z.string().optional(), + phone: z.string(), + wa_id: z.string().optional(), + }) + ) + .optional(), + birthday: z.string().optional(), + addresses: z + .array( + z.object({ + street: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + zip: z.string().optional(), + country: z.string().optional(), + country_code: z.string().optional(), + type: z.string().optional(), + }) + ) + .optional(), + org: z + .object({ + company: z.string().optional(), + department: z.string().optional(), + title: z.string().optional(), + }) + .optional(), + urls: z + .array( + z.object({ + url: z.string().optional(), + type: z.string().optional(), + }) + ) + .optional(), +}); From 6442576a38aa3c6434fab9b23061fade13d04462 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 13:43:18 +0000 Subject: [PATCH 62/71] Added audio, video, document and sticker messages --- examples/whatsapp/src/index.ts | 30 ++ integrations/whatsapp/src/index.ts | 120 ++++++ .../whatsapp/src/internal/requests.ts | 408 +++++++++++++++++- integrations/whatsapp/src/schemas/messages.ts | 79 +++- 4 files changed, 631 insertions(+), 6 deletions(-) diff --git a/examples/whatsapp/src/index.ts b/examples/whatsapp/src/index.ts index 67c66ecccf9..559b409e3aa 100644 --- a/examples/whatsapp/src/index.ts +++ b/examples/whatsapp/src/index.ts @@ -1,12 +1,16 @@ import { Trigger } from "@trigger.dev/sdk"; import { events, + sendAudio, sendContacts, + sendDocument, sendImage, sendLocation, sendReaction, + sendSticker, sendTemplate, sendText, + sendVideo, } from "@trigger.dev/whatsapp"; new Trigger({ @@ -56,6 +60,32 @@ new Trigger({ caption: "This is a genius caption", }); + const videoResponse = await sendVideo("video", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://media.giphy.com/media/5i7umUqAOYYEw/giphy.mp4", + caption: "OMGGGG CATTTT", + }); + + const audioResponse = await sendAudio("audio", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://ssl.gstatic.com/dictionary/static/pronunciation/2022-03-02/audio/wi/wikipedia_en_gb_1.mp3", + }); + + const documentResponse = await sendDocument("doc", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://upload.wikimedia.org/wikipedia/commons/2/20/Re_example.pdf", + caption: "A pdf", + }); + + const stickerResponse = await sendSticker("stick", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://www.tyntec.com/sites/default/files/2020-07/tyntec_rocket_sticker_512px_001_.webp", + }); + const locationResponse = await sendLocation("location", { fromId: event.metadata.phone_number_id, to: event.message.from, diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index 87eb0084fdf..f40fa18d725 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -124,6 +124,126 @@ export async function sendImage( return output; } +export type SendAudioMessageOptions = z.infer< + typeof schemas.messages.SendAudioMessageBodySchema +>; + +export type SendAudioMessageResponse = z.infer< + typeof schemas.messages.SendMessageSuccessResponseSchema +>; + +export async function sendAudio( + key: string, + message: SendAudioMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendAudio outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendAudio", + params: message, + response: { + schema: schemas.messages.SendMessageSuccessResponseSchema, + }, + }); + + return output; +} + +export type SendVideoMessageOptions = z.infer< + typeof schemas.messages.SendVideoMessageBodySchema +>; + +export type SendVideoMessageResponse = z.infer< + typeof schemas.messages.SendMessageSuccessResponseSchema +>; + +export async function sendVideo( + key: string, + message: SendVideoMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendVideo outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendVideo", + params: message, + response: { + schema: schemas.messages.SendMessageSuccessResponseSchema, + }, + }); + + return output; +} + +export type SendDocumentMessageOptions = z.infer< + typeof schemas.messages.SendDocumentMessageBodySchema +>; + +export type SendDocumentMessageResponse = z.infer< + typeof schemas.messages.SendMessageSuccessResponseSchema +>; + +export async function sendDocument( + key: string, + message: SendDocumentMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendDocument outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendDocument", + params: message, + response: { + schema: schemas.messages.SendMessageSuccessResponseSchema, + }, + }); + + return output; +} + +export type SendStickerMessageOptions = z.infer< + typeof schemas.messages.SendStickerMessageBodySchema +>; + +export type SendStickerMessageResponse = z.infer< + typeof schemas.messages.SendMessageSuccessResponseSchema +>; + +export async function sendSticker( + key: string, + message: SendStickerMessageOptions +): Promise { + const run = getTriggerRun(); + + if (!run) { + throw new Error("Cannot call sendSticker outside of a trigger run"); + } + + const output = await run.performRequest(key, { + service: "whatsapp", + endpoint: "message.sendSticker", + params: message, + response: { + schema: schemas.messages.SendMessageSuccessResponseSchema, + }, + }); + + return output; +} + export type SendLocationMessageOptions = z.infer< typeof schemas.messages.SendLocationMessageBodySchema >; diff --git a/integrations/whatsapp/src/internal/requests.ts b/integrations/whatsapp/src/internal/requests.ts index 3820351bb44..535eb09440d 100644 --- a/integrations/whatsapp/src/internal/requests.ts +++ b/integrations/whatsapp/src/internal/requests.ts @@ -13,7 +13,6 @@ import { z } from "zod"; import { SendTemplateMessageBodySchema, SendTemplateMessageRequestBodySchema, - SendMessageSuccessResponseSchema, SendTextMessageBodySchema, SendTextMessageRequestBodySchema, SendReactionMessageBodySchema, @@ -25,6 +24,14 @@ import { SendContactsMessageRequestBodySchema, SendContactsMessageBodySchema, SendMessageResponseSchema, + SendAudioMessageRequestBodySchema, + SendVideoMessageRequestBodySchema, + SendDocumentMessageRequestBodySchema, + SendStickerMessageRequestBodySchema, + SendAudioMessageBodySchema, + SendVideoMessageBodySchema, + SendDocumentMessageBodySchema, + SendStickerMessageBodySchema, } from "../schemas/messages"; const log = debug("trigger:integrations:whatsapp"); @@ -45,6 +52,22 @@ type SendImageMessageRequestBody = z.infer< typeof SendImageMessageRequestBodySchema >; +type SendAudioMessageRequestBody = z.infer< + typeof SendAudioMessageRequestBodySchema +>; + +type SendVideoMessageRequestBody = z.infer< + typeof SendVideoMessageRequestBodySchema +>; + +type SendDocumentMessageRequestBody = z.infer< + typeof SendDocumentMessageRequestBodySchema +>; + +type SendStickerMessageRequestBody = z.infer< + typeof SendStickerMessageRequestBodySchema +>; + type SendLocationMessageRequestBody = z.infer< typeof SendLocationMessageRequestBodySchema >; @@ -90,6 +113,42 @@ export class WhatsAppRequestIntegration implements RequestIntegration { path: "/messages", }); + #sendAudioMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendAudioMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + + #sendVideoMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendVideoMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + + #sendDocumentMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendDocumentMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + + #sendStickerMessageEndpoint = new HttpEndpoint< + typeof SendMessageResponseSchema, + typeof SendStickerMessageRequestBodySchema + >({ + response: SendMessageResponseSchema, + method: "POST", + path: "/messages", + }); + #sendLocationMessageEndpoint = new HttpEndpoint< typeof SendMessageResponseSchema, typeof SendLocationMessageRequestBodySchema @@ -146,6 +205,38 @@ export class WhatsAppRequestIntegration implements RequestIntegration { options.metadata ); } + case "message.sendAudio": { + return this.#sendAudioMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "message.sendVideo": { + return this.#sendVideoMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "message.sendDocument": { + return this.#sendDocumentMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } + case "message.sendSticker": { + return this.#sendStickerMessage( + options.accessInfo, + options.params, + options.cache, + options.metadata + ); + } case "message.sendLocation": { return this.#sendLocationMessage( options.accessInfo, @@ -198,6 +289,34 @@ export class WhatsAppRequestIntegration implements RequestIntegration { properties: [], }; } + case "message.sendAudio": { + const parsedParams = SendAudioMessageBodySchema.parse(params); + return { + title: `Send audio (${parsedParams.url}) to ${parsedParams.to}`, + properties: [], + }; + } + case "message.sendVideo": { + const parsedParams = SendVideoMessageBodySchema.parse(params); + return { + title: `Send video (${parsedParams.url}) to ${parsedParams.to}`, + properties: [], + }; + } + case "message.sendDocument": { + const parsedParams = SendDocumentMessageBodySchema.parse(params); + return { + title: `Send document (${parsedParams.url}) to ${parsedParams.to}`, + properties: [], + }; + } + case "message.sendSticker": { + const parsedParams = SendStickerMessageBodySchema.parse(params); + return { + title: `Send sticker (${parsedParams.url}) to ${parsedParams.to}`, + properties: [], + }; + } case "message.sendLocation": { const parsedParams = SendLocationMessageBodySchema.parse(params); return { @@ -532,6 +651,293 @@ export class WhatsAppRequestIntegration implements RequestIntegration { return performedRequest; } + async #sendAudioMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendAudioMessageBodySchema.parse(params); + + log("message.sendAudio %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendAudioMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "audio", + audio: { + link: parsedParams.url, + }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendAudioMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendAudio failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = !("error" in response.data); + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendAudio performedRequest %O", performedRequest); + + return performedRequest; + } + + async #sendVideoMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendVideoMessageBodySchema.parse(params); + + log("message.sendVideo %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendVideoMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "video", + video: { + link: parsedParams.url, + caption: parsedParams.caption, + }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendVideoMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendVideo failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = !("error" in response.data); + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendVideo performedRequest %O", performedRequest); + + return performedRequest; + } + + async #sendDocumentMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendDocumentMessageBodySchema.parse(params); + + log("message.sendDocument %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendDocumentMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "document", + document: { + link: parsedParams.url, + caption: parsedParams.caption, + }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendDocumentMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendDocument failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = !("error" in response.data); + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendDocument performedRequest %O", performedRequest); + + return performedRequest; + } + + async #sendStickerMessage( + accessInfo: AccessInfo, + params: any, + cache?: CacheService, + metadata?: Record + ): Promise { + const parsedParams = SendStickerMessageBodySchema.parse(params); + + log("message.sendSticker %O", parsedParams); + + const accessToken = getAccessToken(accessInfo); + + const service = new HttpService({ + accessToken, + baseUrl: `${this.baseUrl}/${parsedParams.fromId}`, + }); + + //transform the data from the nice input format into the format that the API expects + const request: SendStickerMessageRequestBody = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: parsedParams.to, + type: "sticker", + sticker: { + link: parsedParams.url, + caption: parsedParams.caption, + }, + context: parsedParams.isReplyTo + ? { message_id: parsedParams.isReplyTo } + : undefined, + }; + + const response = await service.performRequest( + this.#sendStickerMessageEndpoint, + request + ); + + if (!response.success) { + log("message.sendSticker failed %O", response); + + return { + ok: false, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.error, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + } + + const ok = !("error" in response.data); + + const performedRequest = { + ok, + isRetryable: this.#isRetryable(response.statusCode), + response: { + output: response.data, + context: { + statusCode: response.statusCode, + headers: response.headers, + }, + }, + }; + + log("message.sendSticker performedRequest %O", performedRequest); + + return performedRequest; + } + async #sendLocationMessage( accessInfo: AccessInfo, params: any, diff --git a/integrations/whatsapp/src/schemas/messages.ts b/integrations/whatsapp/src/schemas/messages.ts index d707ce9d035..106824a3cc2 100644 --- a/integrations/whatsapp/src/schemas/messages.ts +++ b/integrations/whatsapp/src/schemas/messages.ts @@ -139,11 +139,13 @@ export const SendMessageErrorResponseSchema = z message: z.string(), type: z.string(), code: z.number(), - error_data: z.object({ - messaging_product: z.string(), - details: z.string(), - }), - error_subcode: z.number(), + error_data: z + .object({ + messaging_product: z.string(), + details: z.string(), + }) + .optional(), + error_subcode: z.number().optional(), fbtrace_id: z.string(), }), }) @@ -221,6 +223,73 @@ export const SendImageMessageBodySchema = z.object({ isReplyTo: z.string().optional(), }); +export const SendVideoMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("video"), + video: MediaObject, + context: MessageContextSchema, +}); + +export const SendVideoMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + url: z.string(), + caption: z.string().optional(), + isReplyTo: z.string().optional(), +}); + +export const SendAudioMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("audio"), + audio: MediaObject.omit({ caption: true }), + context: MessageContextSchema, +}); + +export const SendAudioMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + url: z.string(), + isReplyTo: z.string().optional(), +}); + +export const SendDocumentMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("document"), + document: MediaObject, + context: MessageContextSchema, +}); + +export const SendDocumentMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + url: z.string(), + caption: z.string().optional(), + isReplyTo: z.string().optional(), +}); + +export const SendStickerMessageRequestBodySchema = z.object({ + messaging_product: z.literal("whatsapp"), + recipient_type: z.literal("individual"), + to: z.string(), + type: z.literal("sticker"), + sticker: MediaObject, + context: MessageContextSchema, +}); + +export const SendStickerMessageBodySchema = z.object({ + fromId: z.string(), + to: z.string(), + url: z.string(), + caption: z.string().optional(), + isReplyTo: z.string().optional(), +}); + export const SendLocationMessageRequestBodySchema = z.object({ messaging_product: z.literal("whatsapp"), recipient_type: z.literal("individual"), From f46436b903f34ddae0beea36df8fa63d72e392ad Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 14:46:04 +0000 Subject: [PATCH 63/71] =?UTF-8?q?Added=20=E2=80=9Cprettier-plugin-tailwind?= =?UTF-8?q?css=E2=80=9D:=20correct=20order=20for=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/webapp/package.json | 11 +++++------ apps/webapp/prettier.config.js | 3 +++ pnpm-lock.yaml | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 apps/webapp/prettier.config.js diff --git a/apps/webapp/package.json b/apps/webapp/package.json index d4c766e7d4e..042354710ac 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -29,7 +29,6 @@ "db:migrate:deploy": "prisma migrate deploy", "db:migrate:dev": "prisma migrate dev" }, - "prettier": {}, "eslintIgnore": [ "/node_modules", "/build", @@ -63,11 +62,10 @@ "@tailwindcss/forms": "^0.5.2", "@tanstack/react-table": "^8.0.0-alpha.87", "@trigger.dev/common-schemas": "workspace:*", - "integration-catalog": "workspace:*", "@trigger.dev/github": "workspace:*", - "@trigger.dev/slack": "workspace:*", "@trigger.dev/resend": "workspace:*", "@trigger.dev/shopify": "workspace:*", + "@trigger.dev/slack": "workspace:*", "@trigger.dev/whatsapp": "workspace:*", "@typeform/embed-react": "^2.14.1", "@uiw/react-codemirror": "^4.13.2", @@ -84,6 +82,7 @@ "emails": "workspace:*", "express": "^4.18.1", "humanize-duration": "^3.27.3", + "integration-catalog": "workspace:*", "internal-bridge": "workspace:*", "internal-platform": "workspace:*", "internal-pulsar": "workspace:*", @@ -137,8 +136,8 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", - "@trigger.dev/tailwind-config": "workspace:*", "@trigger.dev/integration-sdk": "workspace:*", + "@trigger.dev/tailwind-config": "workspace:*", "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/eslint": "^8.4.6", @@ -176,7 +175,7 @@ "npm-run-all": "^4.1.5", "postcss": "^8.4.14", "prettier": "^2.6.2", - "prettier-plugin-tailwindcss": "^0.1.11", + "prettier-plugin-tailwindcss": "^0.1.13", "prisma": "^4.3.0", "react-date-range": "^1.4.0", "start-server-and-test": "^1.14.0", @@ -191,4 +190,4 @@ "engines": { "node": ">=16.0.0" } -} \ No newline at end of file +} diff --git a/apps/webapp/prettier.config.js b/apps/webapp/prettier.config.js new file mode 100644 index 00000000000..d83c9951c5b --- /dev/null +++ b/apps/webapp/prettier.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require('prettier-plugin-tailwindcss')], +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3556b2e1c20..492b5ef161a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,7 +152,7 @@ importers: postcss-import: ^14.1.0 posthog-js: ^1.31.0 prettier: ^2.6.2 - prettier-plugin-tailwindcss: ^0.1.11 + prettier-plugin-tailwindcss: ^0.1.13 pretty-bytes: ^6.0.0 prism-react-renderer: ^1.3.5 prisma: ^4.3.0 From 3126d890dcbf7a0bea6dbf01a358689fd8085e52 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 21:54:41 +0000 Subject: [PATCH 64/71] Added a WhatsApp example --- apps/docs/examples/examples.mdx | 3 +++ apps/docs/examples/whatsapp.mdx | 46 +++++++++++++++++++++++++++++++++ apps/docs/mint.json | 5 ++-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 apps/docs/examples/whatsapp.mdx diff --git a/apps/docs/examples/examples.mdx b/apps/docs/examples/examples.mdx index c1bd47e1383..df619289540 100644 --- a/apps/docs/examples/examples.mdx +++ b/apps/docs/examples/examples.mdx @@ -20,4 +20,7 @@ You can use these workflows in your product by following the instructions on eac Create a welcome email drip campaign. + + Listen for WhatsApp messages and reply. + diff --git a/apps/docs/examples/whatsapp.mdx b/apps/docs/examples/whatsapp.mdx new file mode 100644 index 00000000000..4e966e3018b --- /dev/null +++ b/apps/docs/examples/whatsapp.mdx @@ -0,0 +1,46 @@ +--- +title: "WhatsApp" +sidebarTitle: "WhatsApp" +description: "Here are some example workflows you could create using the WhatsApp integration." +--- + +## Listen for WhatsApp messages and reply + +Integrations required: WhatsApp + +```ts +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendReaction, sendText } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "whatsapp-webhook", + name: "Listen for WhatsApp messages and reply", + apiKey: "", + //this listens for all WhatsApp messages sent to your WhatsApp Business account + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //these logs will appear on the run page + await ctx.logger.info(`Message data`, event.message); + await ctx.logger.info(`Phone number`, event.contacts[0]); + + //add a 🥰 reaction to the original message + const reactionResponse = await sendReaction("reaction", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + isReplyTo: event.message.id, + emoji: "🥰", + }); + + //send a text message in response + const textResponse = await sendText("text-msg", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + text: "Hello! This is a text sent automatically from https://www.trigger.dev", + }); + + //we support all other types of WhatsApp messages (audio, video, document, location, etc) + }, +}).listen(); +``` diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 2f53c06a1f9..9db85978606 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -55,10 +55,11 @@ "group": "Example workflows", "pages": [ "examples/examples", - "examples/slack", "examples/github", "examples/shopify", - "examples/resend" + "examples/slack", + "examples/resend", + "examples/whatsapp" ] }, { From 22327886ac7b085bba9e0c6fec10fa9e7b2b3745 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 22:55:13 +0000 Subject: [PATCH 65/71] Added message event docs --- .../apis/whatsapp/actions/text-message.mdx | 0 .../apis/whatsapp/events/message.mdx | 106 ++++++++++++++++++ apps/docs/mint.json | 40 +++++++ 3 files changed, 146 insertions(+) create mode 100644 apps/docs/integrations/apis/whatsapp/actions/text-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/events/message.mdx diff --git a/apps/docs/integrations/apis/whatsapp/actions/text-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/text-message.mdx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/docs/integrations/apis/whatsapp/events/message.mdx b/apps/docs/integrations/apis/whatsapp/events/message.mdx new file mode 100644 index 00000000000..c15d446a58e --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/events/message.mdx @@ -0,0 +1,106 @@ +--- +title: "Message Event" +sidebarTitle: "Message" +description: "Trigger a workflow whenever a WhatsApp message is received in your WhatsApp Business Account" +--- + +## Usage + +```ts +import { Trigger } from "@trigger.dev/sdk"; +import { events } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //your workflow code here + }, +}).listen(); +``` + +## Params + + + Your WhatsApp Business Account id + + +## Event + + + The message object has the type of message (e.g. text, video, audio etc) and + the message content. + + + + The contact information of the sender. + + + + Information about the account and phone number that received the message, i.e. + your WhatsApp Business Account details. + + +## Example Workflows + + + +```typescript post a message to your support Slack channel +import { Trigger } from "@trigger.dev/sdk"; +import { events } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //get the user from your database, using the phone number + const user = await db.getUser({ phoneNumber: event.message.from }); + + if (!user) { + await ctx.logger.error(`No user to phone number ${event.message.from}`); + return; + } + + //post a message to your support Slack channel + await slack.postMessage({ + channel: "support", + text: `Support message from ${user.name} (${user.email})\n${event.message.body}`, + }); + }, +}).listen(); +``` + + + +## Example Event payload + +```json +{ + "type": "message", + "message": { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "from": "12345678901", + "text": { + "body": "The quick brown fox jumps over the lazy dog" + }, + "type": "text", + "timestamp": 1675257050000 + }, + "contacts": [ + { + "wa_id": "12345678901", + "profile": { + "name": "Matt Aitken" + } + } + ], + "metadata": { + "phone_number_id": "987654321012", + "display_phone_number": "15550676999" + } +} +``` diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 9db85978606..508027f54e5 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -76,6 +76,23 @@ { "group": "APIs", "pages": [ + { + "group": "GitHub", + "pages": [ + { + "group": "Events", + "pages": [ + "integrations/apis/github/events/issue-comments", + "integrations/apis/github/events/issues", + "integrations/apis/github/events/new-star", + "integrations/apis/github/events/pull-request-comments", + "integrations/apis/github/events/pull-request-reviews", + "integrations/apis/github/events/pull-requests", + "integrations/apis/github/events/push" + ] + } + ] + }, { "group": "Slack", "pages": ["integrations/apis/slack/actions/post-message"] @@ -83,6 +100,29 @@ { "group": "Resend.com", "pages": ["integrations/apis/resend/actions/send-email"] + }, + { + "group": "WhatsApp", + "pages": [ + { + "group": "Events", + "pages": ["integrations/apis/whatsapp/events/message"] + }, + { + "group": "Actions", + "pages": [ + "integrations/apis/whatsapp/actions/text-message", + "integrations/apis/whatsapp/actions/reaction-message", + "integrations/apis/whatsapp/actions/audio-message", + "integrations/apis/whatsapp/actions/video-message", + "integrations/apis/whatsapp/actions/document-message", + "integrations/apis/whatsapp/actions/sticker-message", + "integrations/apis/whatsapp/actions/template-message", + "integrations/apis/whatsapp/actions/location-message", + "integrations/apis/whatsapp/actions/contacts-message" + ] + } + ] } ] }, From 2eea62c3cebe7e5e9cf1125271856ea53ca0b5f4 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 23:24:39 +0000 Subject: [PATCH 66/71] Added lots of the docs for messages --- .../apis/whatsapp/actions/audio-message.mdx | 94 +++++++++++++++ .../whatsapp/actions/document-message.mdx | 99 ++++++++++++++++ .../apis/whatsapp/actions/image-message.mdx | 99 ++++++++++++++++ .../whatsapp/actions/location-message.mdx | 109 ++++++++++++++++++ .../whatsapp/actions/reaction-message.mdx | 95 +++++++++++++++ .../apis/whatsapp/actions/sticker-message.mdx | 98 ++++++++++++++++ .../apis/whatsapp/actions/text-message.mdx | 96 +++++++++++++++ .../apis/whatsapp/actions/video-message.mdx | 99 ++++++++++++++++ 8 files changed, 789 insertions(+) create mode 100644 apps/docs/integrations/apis/whatsapp/actions/audio-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/document-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/image-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/location-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/reaction-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/sticker-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/video-message.mdx diff --git a/apps/docs/integrations/apis/whatsapp/actions/audio-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/audio-message.mdx new file mode 100644 index 00000000000..e031bcaeb6f --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/audio-message.mdx @@ -0,0 +1,94 @@ +--- +title: "Send Audio Message" +sidebarTitle: "Audio Message" +description: "Post a audio message to WhatsApp" +--- + +Send WhatsApp audio messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The url of the audio file to be sent. + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendAudio } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic audio reply to the message + const audioResponse = await sendAudio("audio", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://ssl.gstatic.com/dictionary/static/pronunciation/2022-03-02/audio/wi/wikipedia_en_gb_1.mp3", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/document-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/document-message.mdx new file mode 100644 index 00000000000..2663d513994 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/document-message.mdx @@ -0,0 +1,99 @@ +--- +title: "Send Document Message" +sidebarTitle: "Document Message" +description: "Post a document message to WhatsApp" +--- + +Send WhatsApp document messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The url of the document to be sent. + + + + Optional caption that sits below the video + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendDocument } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic document reply to the message + const documentResponse = await sendDocument("doc", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://upload.wikimedia.org/wikipedia/commons/2/20/Re_example.pdf", + caption: "A pdf for you", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/image-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/image-message.mdx new file mode 100644 index 00000000000..0c8ded7029c --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/image-message.mdx @@ -0,0 +1,99 @@ +--- +title: "Send Image Message" +sidebarTitle: "Image Message" +description: "Post a image message to WhatsApp" +--- + +Send WhatsApp image messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The url of the image to be sent. + + + + Optional caption that sits below the video + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendImage } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic image reply to the message + const imageResponse = await sendImage("image", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://app.trigger.dev/emails/logo.png", + caption: "This is a great caption", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/location-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/location-message.mdx new file mode 100644 index 00000000000..5ccda6f9e30 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/location-message.mdx @@ -0,0 +1,109 @@ +--- +title: "Send a Location Message" +sidebarTitle: "Location Message" +description: "Post a location message to WhatsApp" +--- + +Send WhatsApp location messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The latitude of the location to be sent. + + + + The latitude of the location to be sent. + + + + Name of the location. An address must be specified for this to appear. + + + + The address of the location, in a single string. + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendVideo } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an location message + const locationResponse = await sendLocation("location", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + latitude: 37.422, + longitude: -122.084, + name: "Trigger.dev HQ", + address: "123 Main St, San Francisco, CA 94105", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/reaction-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/reaction-message.mdx new file mode 100644 index 00000000000..ec80438a9c2 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/reaction-message.mdx @@ -0,0 +1,95 @@ +--- +title: "Send a Reaction Message" +sidebarTitle: "Reaction Message" +description: "Post a reaction to a WhatsApp message" +--- + +Send WhatsApp reaction messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The emoji you want to react to the message with 🔥 + + + + This is required: add the id of the message you want to reply to. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendVideo } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic reaction to the message + const reactionResponse = await sendReaction("reaction", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + isReplyTo: event.message.id, + emoji: "🔥", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/sticker-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/sticker-message.mdx new file mode 100644 index 00000000000..c5f863fef07 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/sticker-message.mdx @@ -0,0 +1,98 @@ +--- +title: "Send a Sticker Message" +sidebarTitle: "Sticker Message" +description: "Post a sticker message to WhatsApp" +--- + +Send WhatsApp sticker messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The url of the image to be sent. Please note that only webp files will work for stickers. + + + + Optional caption that sits below the video + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendVideo } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic sticker to the message + const stickerResponse = await sendSticker("stick", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://www.tyntec.com/sites/default/files/2020-07/tyntec_rocket_sticker_512px_001_.webp", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/text-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/text-message.mdx index e69de29bb2d..2a07935c06e 100644 --- a/apps/docs/integrations/apis/whatsapp/actions/text-message.mdx +++ b/apps/docs/integrations/apis/whatsapp/actions/text-message.mdx @@ -0,0 +1,96 @@ +--- +title: "Send Text Message" +sidebarTitle: "Text Message" +description: "Post a text message to WhatsApp" +--- + +Send WhatsApp text messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The text of the message to be sent. + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendText } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic reply to the message + const textResponse = await sendText("text-msg", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + text: "Hello! This is a text sent automatically from https://www.trigger.dev", + //this is optional – if set, the message will appear quoting the original message + isReplyTo: event.message.id, + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/video-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/video-message.mdx new file mode 100644 index 00000000000..4c5b306cfb4 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/video-message.mdx @@ -0,0 +1,99 @@ +--- +title: "Send Video Message" +sidebarTitle: "Video Message" +description: "Post a video message to WhatsApp" +--- + +Send WhatsApp video messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The url of the video to be sent. + + + + Optional caption that sits below the video + + + + Optionally, add the id of the message you want to reply to. It will appear as a reply in the WhatsApp chat. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendVideo } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an automatic video reply to the message + const videoResponse = await sendVideo("video", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + url: "https://media.giphy.com/media/5i7umUqAOYYEw/giphy.mp4", + caption: "OMGGGG CATTTT", + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + From b8b032a5fb6c88b836f979ca5e0b99edf0150ce5 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 08:23:00 +0000 Subject: [PATCH 67/71] Added docs for contact messages and template messages --- .../whatsapp/actions/contacts-message.mdx | 103 ++++++++++ .../whatsapp/actions/template-message.mdx | 178 ++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 apps/docs/integrations/apis/whatsapp/actions/contacts-message.mdx create mode 100644 apps/docs/integrations/apis/whatsapp/actions/template-message.mdx diff --git a/apps/docs/integrations/apis/whatsapp/actions/contacts-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/contacts-message.mdx new file mode 100644 index 00000000000..c3aa6c8b3b0 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/contacts-message.mdx @@ -0,0 +1,103 @@ +--- +title: "Send a Contacts Message" +sidebarTitle: "Contacts Message" +description: "Post a contacts message to WhatsApp" +--- + +Send WhatsApp contacts messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + An array of contacts to be sent. + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendVideo } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send an contacts message + const contactsResponse = await sendContacts("contacts", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + contacts: [ + { + name: { + first_name: "Matt", + last_name: "Aitken", + formatted_name: "Matt Aitken", + }, + phones: [ + { + phone: "+12345678901", + }, + ], + }, + ], + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + diff --git a/apps/docs/integrations/apis/whatsapp/actions/template-message.mdx b/apps/docs/integrations/apis/whatsapp/actions/template-message.mdx new file mode 100644 index 00000000000..09b54fec58f --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/actions/template-message.mdx @@ -0,0 +1,178 @@ +--- +title: "Send Template Message" +sidebarTitle: "Template Message" +description: "Post a template message to WhatsApp" +--- + +Send WhatsApp template messages from your WhatsApp Business Account. + +## Params + + + A unique string. Please see the [Keys and Resumability](/guides/resumability) + doc for more info. + + + + + + The id of the phone number you want to send the message from. You can find this in the your WhatsApp Business dashboard. Note, this is not the phone number. + + + + The phone number you want to send the message to. + + + + The name of the template you want to send. Note that these have to be approved by Meta before they can be used. You can find this in the your WhatsApp Business dashboard. + + + + The language code of the template you want to send, e.g. "en_US". You can find this in the your WhatsApp Business dashboard. + + + + You can customise the message by providing values for variables in the template. There are three sections header, body and buttons. [Full WhatsApp documentation](https://developers.facebook.com/micro_site/url/?click_from_context_menu=true&country=GB&destination=https%3A%2F%2Fdevelopers.facebook.com%2Fdocs%2Fwhatsapp%2Fapi%2Fmessages%2Fmessage-templates%2F%23local&event_type=click&last_nav_impression_id=1gi9JORcnblgd3gKs&max_percent_page_viewed=52&max_viewport_height_px=1272&max_viewport_width_px=2560&orig_http_referrer=https%3A%2F%2Fdevelopers.facebook.com%2Fdocs%2Fwhatsapp%2Fapi%2Fmessages%2Fmessage-templates%2F&orig_request_uri=https%3A%2F%2Fdevelopers.facebook.com%2Fajax%2Fdocs%2Fnav%2F%3Fpath1%3Dwhatsapp%26path2%3Dapi%26path3%3Dmessages%26path4%3Dmessage-templates®ion=emea&scrolled=true&session_id=1p7ElKefs7QDpKC36&site=developers) + + + + + +## Response + + + The contact information of who received the message + + + + An array of message ids that were sent + + + + Always `whatsapp` + + +## Example Workflows + +### Notify Slack on New GitHub Star + + + +```typescript Automated reply to WhatsApp messages +import { Trigger } from "@trigger.dev/sdk"; +import { events, sendText } from "@trigger.dev/whatsapp"; + +new Trigger({ + id: "demo", + on: events.messageEvent({ + accountId: "", + }), + run: async (event, ctx) => { + //send a templated message, with variables + //in this example we have included are the possible variables (mostly commented out) + const templateResponse = await sendTemplate("template-msg", { + fromId: event.metadata.phone_number_id, + to: event.message.from, + //this template must be approved in your WhatsApp Business Account + template: "hello_world", + languageCode: "en_US", + //you only need to include these if your template has variables + parameters: { + //only include this if your header has variables, they can be text, image, video or document + header: [ + { + type: "text", + text: "Matt", + }, + // { + // type: "image", + // image: { + // link: "https://app.trigger.dev/emails/logo.png", + // caption: "This is a logo", + // }, + // }, + // { + // type: "video", + // video: { + // link: "https://media.giphy.com/media/5i7umUqAOYYEw/giphy.mp4", + // caption: "This is a fun video", + // }, + // }, + // { + // type: "document", + // video: { + // link: "https://upload.wikimedia.org/wikipedia/commons/2/20/Re_example.pdf", + // caption: "This is a document", + // }, + // }, + ], + //only include this if the body of your message has variables. They can be text, currency, date_time + body: [ + { + type: "text", + text: "Matt", + }, + // { + // type: "currency", + // currency: { + // amount_1000: 1000, + // code: "USD", + // fallback_value: "1000 USD", + // }, + // }, + // { + // type: "date_time", + // date_time: { + // fallback_value: "2021-01-01", + // }, + // } + ], + //only include this if the buttons of your message have variables. They can be quick_reply or call_to_action + buttons: [ + { + sub_type: "quick_reply", + parameters: [ + { + type: "payload", + payload: "buy", + }, + // { + // type: "text", + // text: "Buy now!", + // }, + ], + }, + { + sub_type: "call_to_action", + parameters: [ + { + type: "text", + text: "Buy now!", + }, + ], + }, + ], + }, + }); + }, +}).listen(); +``` + +```json Example response +{ + "contacts": [ + { + "input": "12345678901", + "wa_id": "12345678901" + } + ], + "messages": [ + { + "id": "wamid.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + ], + "messaging_product": "whatsapp" +} +``` + + From aefbba1f37b47451bae3b4171ce28878dc97937b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 08:27:25 +0000 Subject: [PATCH 68/71] Added an index page to the reference --- apps/docs/examples/whatsapp.mdx | 2 ++ apps/docs/integrations/apis/whatsapp/index.mdx | 9 +++++++++ apps/docs/mint.json | 1 + 3 files changed, 12 insertions(+) create mode 100644 apps/docs/integrations/apis/whatsapp/index.mdx diff --git a/apps/docs/examples/whatsapp.mdx b/apps/docs/examples/whatsapp.mdx index 4e966e3018b..ae298f8cb3a 100644 --- a/apps/docs/examples/whatsapp.mdx +++ b/apps/docs/examples/whatsapp.mdx @@ -4,6 +4,8 @@ sidebarTitle: "WhatsApp" description: "Here are some example workflows you could create using the WhatsApp integration." --- +For more information about the WhatsApp integration, check out the [integration page](/integrations/apis/whatsapp). + ## Listen for WhatsApp messages and reply Integrations required: WhatsApp diff --git a/apps/docs/integrations/apis/whatsapp/index.mdx b/apps/docs/integrations/apis/whatsapp/index.mdx new file mode 100644 index 00000000000..e9e3511fd84 --- /dev/null +++ b/apps/docs/integrations/apis/whatsapp/index.mdx @@ -0,0 +1,9 @@ +--- +title: "WhatsApp reference" +sidebarTitle: "Reference" +description: "Reference for the WhatsApp integration" +--- + +Navigate through the following pages for the full reference for the WhatsApp integration. + +Or view the examples of how to use the WhatsApp integration [here](/examples/whatsapp). diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 508027f54e5..3ded96c3a52 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -104,6 +104,7 @@ { "group": "WhatsApp", "pages": [ + "integrations/apis/whatsapp/index", { "group": "Events", "pages": ["integrations/apis/whatsapp/events/message"] From 42c2a87238227d69606381ee0ba38d11b4766e72 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 08:28:11 +0000 Subject: [PATCH 69/71] Improved the text --- apps/docs/integrations/apis/whatsapp/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/integrations/apis/whatsapp/index.mdx b/apps/docs/integrations/apis/whatsapp/index.mdx index e9e3511fd84..4ae94d9a3ca 100644 --- a/apps/docs/integrations/apis/whatsapp/index.mdx +++ b/apps/docs/integrations/apis/whatsapp/index.mdx @@ -6,4 +6,4 @@ description: "Reference for the WhatsApp integration" Navigate through the following pages for the full reference for the WhatsApp integration. -Or view the examples of how to use the WhatsApp integration [here](/examples/whatsapp). +Or [view the examples](/examples/whatsapp) of how to use the WhatsApp integration. From 49a4b0ce11c9ca076bd99a3f76b1a1a7a3cab5a8 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 08:31:06 +0000 Subject: [PATCH 70/71] Removed changeset files that pulled from dev --- .changeset/fluffy-dragons-mate.md | 8 -------- .changeset/fresh-eyes-run.md | 5 ----- .changeset/honest-dolls-return.md | 8 -------- 3 files changed, 21 deletions(-) delete mode 100644 .changeset/fluffy-dragons-mate.md delete mode 100644 .changeset/fresh-eyes-run.md delete mode 100644 .changeset/honest-dolls-return.md diff --git a/.changeset/fluffy-dragons-mate.md b/.changeset/fluffy-dragons-mate.md deleted file mode 100644 index 692e2d3456d..00000000000 --- a/.changeset/fluffy-dragons-mate.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@trigger.dev/shopify": patch -"@trigger.dev/github": patch -"@trigger.dev/resend": patch -"@trigger.dev/slack": patch ---- - -Remove exports and add main and types to package.json diff --git a/.changeset/fresh-eyes-run.md b/.changeset/fresh-eyes-run.md deleted file mode 100644 index a99234fa3e5..00000000000 --- a/.changeset/fresh-eyes-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/integration-sdk": patch ---- - -Created new @trigger.dev/integration-sdk to support our integration packages diff --git a/.changeset/honest-dolls-return.md b/.changeset/honest-dolls-return.md deleted file mode 100644 index 013bbae4f3c..00000000000 --- a/.changeset/honest-dolls-return.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@trigger.dev/shopify": patch -"@trigger.dev/github": patch -"@trigger.dev/resend": patch -"@trigger.dev/slack": patch ---- - -Initial release of isolated integration packages From 7e1c04ae71ec351f50722005528ae2954403070c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 08:33:13 +0000 Subject: [PATCH 71/71] Removed console.log --- apps/webapp/app/models/workflowRun.server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/webapp/app/models/workflowRun.server.ts b/apps/webapp/app/models/workflowRun.server.ts index e824dc5fbe1..123c6c9fb7a 100644 --- a/apps/webapp/app/models/workflowRun.server.ts +++ b/apps/webapp/app/models/workflowRun.server.ts @@ -48,7 +48,6 @@ export async function startWorkflowRun(id: string, apiKey: string) { await resolveDisconnectionStepsInRun(workflowRun.id); } else { - console.log(`[startWorkflowRun] ${workflowRun.id} is running`); await prisma.workflowRun.update({ where: { id: workflowRun.id }, data: {
  • @@ -84,14 +84,22 @@ export function CreateNewWorkflowNoWorkflows() { Easily authenticate with APIs using the supported integrations below. If there's an integration we don't yet support,{" "} - + {" "} and we'll add it. +
    {getProviders(false).map((provider) => ( Fetch + & + + Webhooks +
    Join the community diff --git a/apps/webapp/app/components/UserProfileMenu.tsx b/apps/webapp/app/components/UserProfileMenu.tsx index 52e73011419..2e02f22d2aa 100644 --- a/apps/webapp/app/components/UserProfileMenu.tsx +++ b/apps/webapp/app/components/UserProfileMenu.tsx @@ -23,7 +23,7 @@ export function UserProfileMenu({ user }: { user: User }) { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - + {user.name ? (
  • ; case "HTTP_ENDPOINT": break; + case "SLACK_INTERACTION": + return ; default: break; } @@ -28,6 +31,43 @@ export function TriggerBody({ trigger }: { trigger: TriggerMetadata }) { const workflowNodeUppercaseClasses = "uppercase text-slate-400 tracking-wide"; +function SlackInteraction({ + trigger, +}: { + trigger: SlackInteractionEventTrigger; +}) { + return ( +
    +
    + + Name + + + {trigger.name} + +
    +
    + + Block + + + {trigger.source.blockId} + +
    +
    + + Action + + + {trigger.source.actionIds.join(", ")} + +
    +
    + ); +} + +// trigger.source.actionIds; + function Webhook({ webhook }: { webhook: WebhookEventTrigger }) { return ( <> diff --git a/apps/webapp/app/components/triggers/TriggerIcons.tsx b/apps/webapp/app/components/triggers/TriggerIcons.tsx index 044bbfa0d89..ea22a7ce871 100644 --- a/apps/webapp/app/components/triggers/TriggerIcons.tsx +++ b/apps/webapp/app/components/triggers/TriggerIcons.tsx @@ -5,6 +5,7 @@ import CustomEvent from "../../assets/images/triggers/custom-event.png"; import HttpEndpoint from "../../assets/images/triggers/http-endpoint.png"; import Schedule from "../../assets/images/triggers/schedule.png"; import Webhook from "../../assets/images/triggers/webhook.png"; +import SlackInteraction from "../../assets/images/triggers/slack-interaction.png"; import { triggerLabel } from "./triggerLabel"; type TriggerType = Workflow["type"]; @@ -52,6 +53,14 @@ export function TriggerTypeIcon({ return ( {triggerLabel(type)} ); + case "SLACK_INTERACTION": + return ( + {triggerLabel(type)} + ); default: return null; } diff --git a/apps/webapp/app/components/triggers/triggerLabel.tsx b/apps/webapp/app/components/triggers/triggerLabel.tsx index 07f1bca99e3..5558fcb046d 100644 --- a/apps/webapp/app/components/triggers/triggerLabel.tsx +++ b/apps/webapp/app/components/triggers/triggerLabel.tsx @@ -12,6 +12,8 @@ export function triggerLabel(type: TriggerType) { return "HTTP endpoint"; case "SCHEDULE": return "Scheduled"; + case "SLACK_INTERACTION": + return "Slack interaction"; default: return type; } diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 43d8bde6a3c..dda42842f76 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({ PULSAR_ISSUER_URL: z.string().optional(), PULSAR_AUDIENCE: z.string().optional(), PULSAR_DEBUG: z.string().optional(), + INTERNAL_TRIGGER_API_KEY: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/models/workflowListPresenter.server.ts b/apps/webapp/app/models/workflowListPresenter.server.ts index 11831225574..956610654b7 100644 --- a/apps/webapp/app/models/workflowListPresenter.server.ts +++ b/apps/webapp/app/models/workflowListPresenter.server.ts @@ -1,5 +1,8 @@ -import type { SchedulerSource } from ".prisma/client"; -import { ScheduleSourceSchema } from "@trigger.dev/common-schemas"; +import type { SchedulerSource, InternalSource } from ".prisma/client"; +import { + ScheduleSourceSchema, + SlackInteractionSourceSchema, +} from "@trigger.dev/common-schemas"; import cronstrue from "cronstrue"; import type { DisplayProperties } from "internal-integrations"; import { github } from "internal-integrations"; @@ -60,7 +63,8 @@ export class WorkflowListPresenter { trigger: triggerProperties( workflow, workflow.externalSource ?? undefined, - workflow.schedulerSources[0] ?? undefined + workflow.schedulerSources[0] ?? undefined, + workflow.internalSources[0] ?? undefined ), integrations: { source: workflow.service @@ -105,6 +109,17 @@ function getWorkflows( orderBy: { createdAt: "desc" }, take: 1, }, + internalSources: { + select: { + source: true, + type: true, + }, + where: { + environmentId, + }, + orderBy: { createdAt: "desc" }, + take: 1, + }, runs: { select: { finishedAt: true, @@ -124,7 +139,8 @@ function getWorkflows( function triggerProperties( workflow: Pick, externalSource?: Pick, - schedulerSource?: Pick + schedulerSource?: Pick, + internalSource?: Pick ): { type: Workflow["type"]; typeTitle: string; @@ -157,7 +173,14 @@ function triggerProperties( }; } case "SCHEDULE": { - invariant(schedulerSource, "Scheduler source is required for schedule"); + if (!schedulerSource) { + return { + type: workflow.type, + typeTitle: "Schedule", + title: "Not configured", + }; + } + const source = ScheduleSourceSchema.parse(schedulerSource.schedule); if ("rateOf" in source) { @@ -205,6 +228,29 @@ function triggerProperties( typeTitle: "Custom event", title: `on: ${workflow.eventNames.join(", ")}`, }; + case "SLACK_INTERACTION": { + if (!internalSource) { + return { + type: workflow.type, + typeTitle: "Slack interaction", + title: "on: Slack interaction", + }; + } + + const slackSource = SlackInteractionSourceSchema.parse( + internalSource.source + ); + + return { + type: workflow.type, + typeTitle: "Slack interaction", + title: `block_id = ${slackSource.blockId}`, + properties: + slackSource.actionIds.length > 0 + ? [{ key: "Action ID", value: slackSource.actionIds.join(", ") }] + : undefined, + }; + } default: { return { type: workflow.type, diff --git a/apps/webapp/app/models/workflowRun.server.ts b/apps/webapp/app/models/workflowRun.server.ts index aaa6194939c..123c6c9fb7a 100644 --- a/apps/webapp/app/models/workflowRun.server.ts +++ b/apps/webapp/app/models/workflowRun.server.ts @@ -1,14 +1,16 @@ -import type { WorkflowRun, WorkflowRunStep } from ".prisma/client"; -import type { WorkflowRunStatus } from ".prisma/client"; +import type { + WorkflowRun, + WorkflowRunStatus, + WorkflowRunStep, +} from ".prisma/client"; import type { CustomEventSchema, ErrorSchema, LogMessageSchema, } from "@trigger.dev/common-schemas"; -import { ulid } from "ulid"; import type { z } from "zod"; import { prisma } from "~/db.server"; -import { IngestEvent } from "~/services/events/ingest.server"; +import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server"; import { createStepOnce } from "./workflowRunStep.server"; export type { WorkflowRun, WorkflowRunStep, WorkflowRunStatus }; @@ -160,20 +162,12 @@ export async function triggerEventInRun( return; } - const ingestService = new IngestEvent(); - - await ingestService.call( - { - id: ulid(), - name: event.name, - type: "CUSTOM_EVENT", - service: "trigger", - payload: event.payload, - context: event.context, - apiKey: workflowRun.environment.apiKey, - }, - workflowRun.environment.organization - ); + const ingestService = new IngestCustomEvent(); + + await ingestService.call({ + apiKey: workflowRun.environment.apiKey, + event, + }); await prisma.workflowRunStep.update({ where: { id: step.step.id }, @@ -228,13 +222,18 @@ async function findWorkflowRunScopedToApiKey(id: string, apiKey: string) { export async function getMostRecentWorkflowRun({ workflowSlug, + organizationSlug, }: { workflowSlug: string; + organizationSlug: string; }) { return prisma.workflowRun.findFirst({ where: { workflow: { slug: workflowSlug, + organization: { + slug: organizationSlug, + }, }, }, include: { diff --git a/apps/webapp/app/models/workflowRunListPresenter.server.ts b/apps/webapp/app/models/workflowRunListPresenter.server.ts index 470c5b8aad7..e9fa5e4e0d3 100644 --- a/apps/webapp/app/models/workflowRunListPresenter.server.ts +++ b/apps/webapp/app/models/workflowRunListPresenter.server.ts @@ -54,6 +54,14 @@ export class WorkflowRunListPresenter { where: { workflow: { slug: workflowSlug, + organization: { + slug: organizationSlug, + users: { + some: { + id: userId, + }, + }, + }, }, environment: { slug: environmentSlug, diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx index 0b143e9165c..65b3c33ab9d 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx @@ -9,10 +9,10 @@ import { } from "@heroicons/react/24/outline"; import { ArrowPathRoundedSquareIcon, + ChatBubbleOvalLeftEllipsisIcon, CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, - ChatBubbleOvalLeftEllipsisIcon, } from "@heroicons/react/24/solid"; import { useFetcher } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/server-runtime"; @@ -623,6 +623,40 @@ function CustomEventStep({ event }: { event: StepType }) { {event.input.name} + {"delay" in event.input && event.input.delay && ( + <> + + Delay + + + {"seconds" in event.input.delay ? ( + <> + {event.input.delay.seconds}{" "} + {event.input.delay.seconds > 1 ? "seconds" : "second"} + + ) : "minutes" in event.input.delay ? ( + <> + {event.input.delay.minutes}{" "} + {event.input.delay.minutes > 1 ? "minutes" : "minute"} + + ) : "hours" in event.input.delay ? ( + <> + {event.input.delay.hours}{" "} + {event.input.delay.hours > 1 ? "hours" : "hour"} + + ) : "days" in event.input.delay ? ( + <> + {event.input.delay.days}{" "} + {event.input.delay.days > 1 ? "days" : "day"} + + ) : "until" in event.input.delay ? ( + <>Until {event.input.delay.until} + ) : ( + <> + )} + + + )} Payload {event.input.context && ( @@ -678,7 +712,11 @@ function IntegrationRequestStep({ {request.input && ( <> - + )}