From 4adad0041ae0fee6bc61d59109e61346788c37fb Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 29 Jan 2023 12:28:58 -0800 Subject: [PATCH 01/83] 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 02/83] 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 03/83] 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 04/83] 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 05/83] 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 06/83] 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 07/83] 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 08/83] 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 09/83] 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 10/83] 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 11/83] 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 12/83] 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 13/83] 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 14/83] 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 15/83] =?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 16/83] =?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 17/83] 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 18/83] =?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 19/83] =?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 20/83] 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 21/83] 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 22/83] 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 23/83] 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 24/83] 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 25/83] 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 26/83] 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 27/83] 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 28/83] 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 29/83] 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 30/83] 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 31/83] 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 32/83] 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 33/83] 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 34/83] 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 35/83] 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 36/83] 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 37/83] 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 38/83] 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 39/83] 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 40/83] 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 41/83] 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 42/83] 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 43/83] 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 44/83] =?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 45/83] 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 46/83] 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 47/83] 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 48/83] 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 49/83] 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 50/83] 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 51/83] =?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 6ddaf067a96fa8e046464b7264b014afd5276fcf Mon Sep 17 00:00:00 2001 From: D-K-P Date: Wed, 1 Feb 2023 15:21:16 +0000 Subject: [PATCH 52/83] Added posthog to mint.json --- apps/docs/mint.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 2f53c06a1f9..2a647d2fbfb 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -148,5 +148,12 @@ "auth": { "method": "bearer" } - } + }, + "analytics": { + "posthog": { + "apiKey": "required", + "apiHost": "optional" + } +} } + From 37d331588d23a36b5f670723bea535f2b912b59b Mon Sep 17 00:00:00 2001 From: D-K-P Date: Wed, 1 Feb 2023 15:24:20 +0000 Subject: [PATCH 53/83] Added package instructions for the integrations to the docs --- apps/docs/examples/github.mdx | 28 ++++++++++++++++++++ apps/docs/examples/resend.mdx | 47 ++++++++++++++++++++++++++-------- apps/docs/examples/shopify.mdx | 24 +++++++++++++++++ apps/docs/examples/slack.mdx | 26 +++++++++++++++++++ 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/apps/docs/examples/github.mdx b/apps/docs/examples/github.mdx index f7db5254f3a..c7ae3c1a4c9 100644 --- a/apps/docs/examples/github.mdx +++ b/apps/docs/examples/github.mdx @@ -4,8 +4,36 @@ sidebarTitle: "GitHub" description: "Here are some example workflows you could create using the GitHub integration." --- + +To use the GitHub integration you must first install the required packages: + + + + +```bash +npm install @trigger.dev/github +``` + + + + +```bash +pnpm install @trigger.dev/github +``` + + + + +```bash +yarn add @trigger.dev/github +``` + + + + 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 diff --git a/apps/docs/examples/resend.mdx b/apps/docs/examples/resend.mdx index c2ee3e37797..890322fe162 100644 --- a/apps/docs/examples/resend.mdx +++ b/apps/docs/examples/resend.mdx @@ -8,6 +8,32 @@ description: "Here are some example workflows you could create using the Resend Resend.com is currently in private beta but you can request early access [here](https://resend.com/). +To use the Resend integration you must first install the required packages: + + + + +```bash +npm install @trigger.dev/resend +``` + + + + +```bash +pnpm install @trigger.dev/resend +``` + + + + +```bash +yarn add @trigger.dev/resend +``` + + + + ## Welcome email drip campaign This workflow will get triggered and a Slack notification and welcom email will be sent straight away. @@ -15,17 +41,6 @@ This workflow will get triggered and a Slack notification and welcom email will Integrations required: Resend, Slack - - - ```ts import { customEvent, Trigger, sendEvent } from "@trigger.dev/sdk"; import * as resend from "@trigger.dev/resend"; @@ -90,3 +105,13 @@ new Trigger({ }, }).listen(); ``` + + \ No newline at end of file diff --git a/apps/docs/examples/shopify.mdx b/apps/docs/examples/shopify.mdx index df6c60743e7..0ece2505e1d 100644 --- a/apps/docs/examples/shopify.mdx +++ b/apps/docs/examples/shopify.mdx @@ -4,6 +4,30 @@ sidebarTitle: "Shopify" description: "Here are some example workflows you could create using the Shopify integration." --- +To use the Resend integration you must first install the required packages: + + + + +```bash +npm install @trigger.dev/shopify +``` + + + + +```bash +pnpm install @trigger.dev/shopify +``` + + + + +```bash +yarn add @trigger.dev/shopify +``` + + ## Create a new product Integrations required: Shopify diff --git a/apps/docs/examples/slack.mdx b/apps/docs/examples/slack.mdx index c464171c95b..8895e7bf020 100644 --- a/apps/docs/examples/slack.mdx +++ b/apps/docs/examples/slack.mdx @@ -4,6 +4,32 @@ sidebarTitle: "Slack" description: "Here are some example workflows you could create using the Slack integration." --- +To use the Slack integration you must first install the required packages: + + + + +```bash +npm install @trigger.dev/slack +``` + + + + +```bash +pnpm install @trigger.dev/slack +``` + + + + +```bash +yarn add @trigger.dev/slack +``` + + + + ## Post to Slack when a GitHub issue is created or modified Integrations required: Slack, GitHub From 60abac1bc0ac2fa369eb402353d485d9cf7118fa Mon Sep 17 00:00:00 2001 From: D-K-P Date: Wed, 1 Feb 2023 15:32:02 +0000 Subject: [PATCH 54/83] Updated mint.json --- apps/docs/mint.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 2a647d2fbfb..665e32fffa9 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -151,8 +151,7 @@ }, "analytics": { "posthog": { - "apiKey": "required", - "apiHost": "optional" + "apiKey": "phc_cU8Ow5tDcy9alp09yWYpad98CHafOsfsXK3HzLM6uy0" } } } From 3126d890dcbf7a0bea6dbf01a358689fd8085e52 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Feb 2023 21:54:41 +0000 Subject: [PATCH 55/83] 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 56/83] 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 57/83] 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 58/83] 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 59/83] 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 60/83] 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 61/83] 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 62/83] 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: { From c37edf970d17f18f17b236ecc08fe3c349d33e7f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 10:49:54 +0000 Subject: [PATCH 63/83] Added workflowId and appOrigin to the @trigger.dev/sdk run metadata --- apps/webapp/app/services/messageBroker.server.ts | 1 + apps/wss/src/runController.ts | 2 ++ apps/wss/src/server.ts | 1 + packages/internal-bridge/src/schemas/host.ts | 1 + packages/internal-platform/src/messages/catalogs/triggers.ts | 1 + packages/trigger-sdk/src/client.ts | 2 ++ packages/trigger-sdk/src/localStorage.ts | 2 ++ 7 files changed, 10 insertions(+) diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts index 1d4f04cc161..7ac291d8a30 100644 --- a/apps/webapp/app/services/messageBroker.server.ts +++ b/apps/webapp/app/services/messageBroker.server.ts @@ -688,6 +688,7 @@ function createTaskQueue() { "x-workflow-run-id": run.id, "x-ttl": run.workflow.triggerTtlInSeconds, "x-is-test": run.isTest ? "true" : "false", + "x-app-origin": env.APP_ORIGIN, }, { eventTimestamp: run.event.timestamp.getTime(), diff --git a/apps/wss/src/runController.ts b/apps/wss/src/runController.ts index 6a68016c7e9..c009b7819d2 100644 --- a/apps/wss/src/runController.ts +++ b/apps/wss/src/runController.ts @@ -25,6 +25,7 @@ export type WorkflowRunControllerOptions = { apiKey: string; organizationId: string; isTest: boolean; + appOrigin: string; }; }; @@ -39,6 +40,7 @@ export class WorkflowRunController { apiKey: string; organizationId: string; isTest: boolean; + appOrigin: string; }; #logger: Logger; diff --git a/apps/wss/src/server.ts b/apps/wss/src/server.ts index 2cf874cdaa1..4c663d628e5 100644 --- a/apps/wss/src/server.ts +++ b/apps/wss/src/server.ts @@ -420,6 +420,7 @@ export class TriggerServer { organizationId: properties["x-org-id"], environment: properties["x-env"], apiKey: properties["x-api-key"], + appOrigin: properties["x-app-origin"], isTest: typeof properties["x-is-test"] === "string" ? properties["x-is-test"] === "true" diff --git a/packages/internal-bridge/src/schemas/host.ts b/packages/internal-bridge/src/schemas/host.ts index 2209197e711..152190eeea2 100644 --- a/packages/internal-bridge/src/schemas/host.ts +++ b/packages/internal-bridge/src/schemas/host.ts @@ -15,6 +15,7 @@ export const HostRPCSchema = { organizationId: z.string(), apiKey: z.string(), isTest: z.boolean().default(false), + appOrigin: z.string(), }), }), response: z.boolean(), diff --git a/packages/internal-platform/src/messages/catalogs/triggers.ts b/packages/internal-platform/src/messages/catalogs/triggers.ts index 53a6d02fe9c..4495dab329e 100644 --- a/packages/internal-platform/src/messages/catalogs/triggers.ts +++ b/packages/internal-platform/src/messages/catalogs/triggers.ts @@ -8,6 +8,7 @@ const Catalog = { properties: WorkflowRunEventPropertiesSchema.extend({ "x-ttl": z.coerce.number().optional(), "x-is-test": z.string().default("false"), + "x-app-origin": z.string().default("https://app.trigger.dev"), }), }, }; diff --git a/packages/trigger-sdk/src/client.ts b/packages/trigger-sdk/src/client.ts index de8b6bad7ba..327ed5ad562 100644 --- a/packages/trigger-sdk/src/client.ts +++ b/packages/trigger-sdk/src/client.ts @@ -469,6 +469,8 @@ export class TriggerClient { }); }, fetch: fetchFunction, + workflowId: data.meta.workflowId, + appOrigin: data.meta.appOrigin, }, () => { this.#logger.debug("Running trigger..."); diff --git a/packages/trigger-sdk/src/localStorage.ts b/packages/trigger-sdk/src/localStorage.ts index 2a61f417719..100abae43f6 100644 --- a/packages/trigger-sdk/src/localStorage.ts +++ b/packages/trigger-sdk/src/localStorage.ts @@ -18,6 +18,8 @@ type TriggerRunLocalStorage = { ) => Promise>; sendEvent: (key: string, event: TriggerCustomEvent) => Promise; fetch: TriggerFetch; + workflowId: string; + appOrigin: string; }; export const triggerRunLocalStorage = From 6495b67d1fd79b225f46a600e6c30c321b97f442 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 10:50:41 +0000 Subject: [PATCH 64/83] Make image, document, video etc use the EventMediaObjectSchema --- .../whatsapp/src/schemas/messageEvents.ts | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/integrations/whatsapp/src/schemas/messageEvents.ts b/integrations/whatsapp/src/schemas/messageEvents.ts index aec37d17753..f913c1ccf9f 100644 --- a/integrations/whatsapp/src/schemas/messageEvents.ts +++ b/integrations/whatsapp/src/schemas/messageEvents.ts @@ -28,33 +28,27 @@ const textMessageEventSchema = z.object({ text: z.object({ body: z.string() }), }); +export const EventMediaObjectSchema = z.object({ + id: z.string(), + mime_type: z.string(), + sha256: z.string().optional(), +}); + const audioMessageEventSchema = z.object({ type: z.literal("audio"), - audio: z.object({ - id: z.string(), - mime_type: z.string(), - sha256: z.string().optional(), + audio: EventMediaObjectSchema.extend({ 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(), - }), + video: EventMediaObjectSchema, }); 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(), - }), + image: EventMediaObjectSchema.extend({ caption: z.string().optional() }), }); const reactionMessageEventSchema = z.object({ @@ -67,20 +61,12 @@ const reactionMessageEventSchema = z.object({ const stickerMessageEventSchema = z.object({ type: z.literal("sticker"), - sticker: z.object({ - id: z.string(), - sha256: z.string(), - animated: z.boolean(), - mime_type: z.string(), - }), + sticker: EventMediaObjectSchema.extend({ animated: z.boolean() }), }); const documentMessageEventSchema = z.object({ type: z.literal("document"), - document: z.object({ - id: z.string(), - mime_type: z.string(), - sha256: z.string().optional(), + document: EventMediaObjectSchema.extend({ filename: z.string().optional(), caption: z.string().optional(), }), From 05b0bd9fb44a82a73f8825ed43267d6e18ae5d87 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 10:51:49 +0000 Subject: [PATCH 65/83] Add jsx-slack to the WhatsApp example package --- examples/whatsapp/package.json | 4 +++- examples/whatsapp/tsconfig.json | 5 ++++- pnpm-lock.yaml | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/whatsapp/package.json b/examples/whatsapp/package.json index 5f74d45c81a..183e6dae78d 100644 --- a/examples/whatsapp/package.json +++ b/examples/whatsapp/package.json @@ -4,8 +4,10 @@ "version": "0.0.1", "description": "Example trigger.dev workflow that uses the WhatsApp Webhook integration", "dependencies": { - "@trigger.dev/whatsapp": "workspace:*", "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/slack": "workspace:*", + "@trigger.dev/whatsapp": "workspace:*", + "jsx-slack": "^5.3.0", "zod": "^3.20.2" }, "devDependencies": { diff --git a/examples/whatsapp/tsconfig.json b/examples/whatsapp/tsconfig.json index 00ad22f1a85..c7eccd9c9fd 100644 --- a/examples/whatsapp/tsconfig.json +++ b/examples/whatsapp/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "@trigger.dev/tsconfig/examples.json", "include": ["src/**/*.ts"], - "exclude": ["node_modules", "**/*.test.*"] + "exclude": ["node_modules", "**/*.test.*"], + "compilerOptions": { + "jsx": "react-jsx" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 492b5ef161a..47a171ccfd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -566,14 +566,18 @@ importers: examples/whatsapp: specifiers: '@trigger.dev/sdk': workspace:* + '@trigger.dev/slack': workspace:* '@trigger.dev/tsconfig': workspace:* '@trigger.dev/whatsapp': workspace:* '@types/node': '16' + jsx-slack: ^5.3.0 tsx: ^3.12.0 zod: ^3.20.2 dependencies: '@trigger.dev/sdk': link:../../packages/trigger-sdk + '@trigger.dev/slack': link:../../integrations/slack '@trigger.dev/whatsapp': link:../../integrations/whatsapp + jsx-slack: 5.3.0 zod: 3.20.2 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig From d6c7648bf16f8d1a6012bda7627f4da061ba68ed Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Feb 2023 13:44:31 +0000 Subject: [PATCH 66/83] WhatsApp media urls and geting files is working --- .../webapp/app/models/apiConnection.server.ts | 41 ++++++++ ...$serviceIdentifier.$workflowId.$mediaId.ts | 93 +++++++++++++++++++ .../performIntegrationRequest.server.ts | 15 +-- examples/whatsapp/package.json | 3 +- examples/whatsapp/src/whatsapp-slack.tsx | 85 +++++++++++++++++ integrations/whatsapp/src/index.ts | 32 +++++++ integrations/whatsapp/src/internal.ts | 2 +- .../whatsapp/src/internal/requests.ts | 42 +++++++++ 8 files changed, 304 insertions(+), 9 deletions(-) create mode 100644 apps/webapp/app/routes/api/v1/internal/media/$serviceIdentifier.$workflowId.$mediaId.ts create mode 100644 examples/whatsapp/src/whatsapp-slack.tsx diff --git a/apps/webapp/app/models/apiConnection.server.ts b/apps/webapp/app/models/apiConnection.server.ts index 9e6f11de3c6..2de0400e1a9 100644 --- a/apps/webapp/app/models/apiConnection.server.ts +++ b/apps/webapp/app/models/apiConnection.server.ts @@ -1,6 +1,7 @@ import type { APIConnection, Organization } from ".prisma/client"; import { z } from "zod"; import { prisma } from "~/db.server"; +import { Workflow } from "./workflow.server"; export { APIConnection }; @@ -117,3 +118,43 @@ export async function updateApiConnectionTitle({ }, }); } + +export async function getApiConnectionsForWorkflow({ + workflowId, +}: { + workflowId: Workflow["id"]; +}) { + const workflow = await prisma.workflow.findUnique({ + where: { + id: workflowId, + }, + select: { + externalSource: { + select: { + service: true, + connection: true, + }, + }, + externalServices: { + select: { + service: true, + connection: true, + }, + }, + }, + }); + + if (!workflow) return []; + + let connections: APIConnection[] = []; + if (workflow.externalSource?.connection) { + connections.push(workflow.externalSource.connection); + } + workflow.externalServices.forEach((s) => { + if (s.connection) { + connections.push(s.connection); + } + }); + + return connections; +} diff --git a/apps/webapp/app/routes/api/v1/internal/media/$serviceIdentifier.$workflowId.$mediaId.ts b/apps/webapp/app/routes/api/v1/internal/media/$serviceIdentifier.$workflowId.$mediaId.ts new file mode 100644 index 00000000000..2a71c290dcd --- /dev/null +++ b/apps/webapp/app/routes/api/v1/internal/media/$serviceIdentifier.$workflowId.$mediaId.ts @@ -0,0 +1,93 @@ +import type { LoaderArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { getApiConnectionsForWorkflow } from "~/models/apiConnection.server"; +import { getAccessInfo } from "~/services/accessInfo.server"; + +const paramsSchema = z.object({ + serviceIdentifier: z.string(), + workflowId: z.string(), + mediaId: z.string(), +}); + +export async function loader({ request, params }: LoaderArgs) { + const { serviceIdentifier, workflowId, mediaId } = paramsSchema.parse(params); + + const connections = await getApiConnectionsForWorkflow({ workflowId }); + const relevantConnection = connections.find( + (c) => c.apiIdentifier === serviceIdentifier + ); + if (!relevantConnection) { + return { + status: 404, + body: `Could not find connection with serviceIdentifier ${serviceIdentifier}`, + }; + } + + if (relevantConnection.status !== "CONNECTED") { + return { + status: 500, + body: `Connection with serviceIdentifier ${serviceIdentifier} is not connected`, + }; + } + + const accessInfo = await getAccessInfo(relevantConnection); + + switch (serviceIdentifier) { + case "whatsapp": { + if (accessInfo?.type !== "api_key") { + return { status: 500, body: "Invalid access info" }; + } + + const accessToken = accessInfo.api_key; + const mediaUrlResponse = await fetch( + `https://graph.facebook.com/v15.0/${mediaId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + if (!mediaUrlResponse.ok) { + return { + status: mediaUrlResponse.status, + body: await mediaUrlResponse.text(), + }; + } + + const json = await mediaUrlResponse.json(); + + if (!json.url) { + return { + status: 500, + body: `Could not find url in response: ${JSON.stringify(json)}`, + }; + } + + const media = await fetch(json.url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!media.ok) { + return { + status: media.status, + body: await media.text(), + }; + } + + return new Response(media.body, { + headers: { + "Content-Type": json.mime_type, + "Content-Disposition": `attachment;`, + }, + }); + } + } + + return { + status: 500, + body: `Unknown serviceIdentifier ${serviceIdentifier}`, + }; +} diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts index 4c9f5e006ec..12a9a3928b1 100644 --- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts +++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts @@ -4,10 +4,6 @@ import type { NormalizedResponse, PerformedRequestResponse, } from "@trigger.dev/integration-sdk"; - -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"; @@ -68,7 +64,8 @@ export class PerformIntegrationRequest { integrationRequest.externalService.connection.apiIdentifier, accessInfo, integrationRequest, - cache + cache, + integrationRequest.externalService.workflowId ); if (performedRequest.ok) { @@ -226,7 +223,8 @@ export class PerformIntegrationRequest { service: string, accessInfo: AccessInfo, integrationRequest: IntegrationRequest, - cache: CacheService + cache: CacheService, + workflowId: string ): Promise { const integrationInfo = getIntegrations(true).find( (i) => i.metadata.slug === service @@ -247,7 +245,10 @@ export class PerformIntegrationRequest { endpoint: integrationRequest.endpoint, params: integrationRequest.params, cache, - metadata: { requestId: integrationRequest.id }, + metadata: { + requestId: integrationRequest.id, + workflowId: workflowId, + }, }); } } diff --git a/examples/whatsapp/package.json b/examples/whatsapp/package.json index 183e6dae78d..9beddda001a 100644 --- a/examples/whatsapp/package.json +++ b/examples/whatsapp/package.json @@ -16,6 +16,7 @@ "tsx": "^3.12.0" }, "scripts": { - "dev": "tsx src/index.ts" + "dev": "tsx src/index.ts", + "whatsapp-slack": "tsx src/whatsapp-slack.tsx" } } diff --git a/examples/whatsapp/src/whatsapp-slack.tsx b/examples/whatsapp/src/whatsapp-slack.tsx new file mode 100644 index 00000000000..d9816389e03 --- /dev/null +++ b/examples/whatsapp/src/whatsapp-slack.tsx @@ -0,0 +1,85 @@ +/** @jsxImportSource jsx-slack */ +import { Trigger } from "@trigger.dev/sdk"; +import { + events, + sendText, + getMediaUrl, + MessageEventMessage, +} from "@trigger.dev/whatsapp"; +import JSXSlack, { + Actions, + Blocks, + Button, + Section, + Header, + Context, + Image, + Video, +} from "jsx-slack"; +import * as slack from "@trigger.dev/slack"; + +const SLACK_BLOCK_ID = "launch.modal"; + +const dateFormatter = new Intl.DateTimeFormat("en-US", { + timeStyle: "short", + dateStyle: "short", +}); + +new Trigger({ + id: "whatsapp-to-slack", + name: "WhatsApp to Slack", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", + logLevel: "debug", + on: events.messageEvent({ + accountId: "114848614845931", + }), + run: async (event, ctx) => { + ctx.logger.debug("event", event); + let messageBody = <>; + + switch (event.message.type) { + case "text": { + messageBody =
    {event.message.text.body}
    ; + break; + } + case "image": { + const mediaUrl = await getMediaUrl(`getImageUrl`, event.message.image); + messageBody = ( + {event.message.image.caption + ); + break; + } + case "video": { + const mediaUrl = await getMediaUrl(`getVideoUrl`, event.message.video); + messageBody = ( +
  • -
    +
    Add an API integration -
    +
    {integrations.map((integration) => ( {(status) => ( @@ -104,14 +104,14 @@ export default function Integrations() { ))} -
    +
    Need an integration? - + Let us know!
    @@ -131,25 +131,25 @@ function AddButtonContent({ }) { return ( <> -
    - +
    + {integration.name}
    {status === "loading" ? ( - - Connecting to{" "} - {integration.name} - +
    + Connecting to + {integration.name} +
    ) : ( - - Connect to{" "} - {integration.name} - +
    + Connect to + {integration.name} +
    )} ); From 517d50242fec1c643bf346b728551f0c9e03b0dc Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 14:25:23 +0000 Subject: [PATCH 68/83] Updated whatsapp icon --- apps/webapp/public/integrations/whatsapp.png | Bin 6551 -> 31619 bytes integrations/whatsapp/src/internal.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/public/integrations/whatsapp.png b/apps/webapp/public/integrations/whatsapp.png index 01adff2cce5a6ac0e4a9f839d631d015c164b0d7..b1d7a02fd6ce8e1072df5c5998fd32b2ace65a20 100644 GIT binary patch literal 31619 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 SxOP_n0000&|X~W zc8iIZAt{WThGEraS3m~0F)RH8XcJizs$flar7?nu7?Vu^3(YoL3;eMTnh2NdVq9T$ zcvIZqa?J+Ai!ZZKmI3CTr_b-)xiByH_1tsM`JHpVpM)|DV>*A#%=i9&&+mE8If@|E z+Pc$8%cd5~q83F~vhaRenLlf4DfQneqO9|VR=rx~dil+M^S%=unJhtQC<3ukJJT&! zM^iFkSt(7FG{2^(d@xhyJJ+Ji1hgLJ|D#viDoe`l)ugiKhvZYGRSyTfsgHVk7G-(~ zL_mOr;7U4*th9gDIz^V6!Xf3*l0Z(tweBHJhfUSbA|qf69|8>hd8v?##voT>xHqYvaacj zO==s$Tpt9uPEMsALuswlN`%&}fiU;kyRdGq4Fc?vv!>OmQ(0nIFiln(S>@ zGgk)zt^rzP|L{SR9MO8T*ySO>RmgAqSS!sSW)-Tfzj*j%wx)I^$X<^ikAwi1k?Kw- z`?XqyHd%iLZm$vKp%CCAob|1;R+?A-P6B~+#{623but1xgi~=zZNd6BEOe%c^zt}Y zWCWNBhc47cO==lI=)}3Tj&*=BoQjq>2069?p5EQwl~D8|f*UQo{bXy}bo_z68z}~2 z7vR)e=~RDCy@+*gv^h5{u+-|K^WWY@5f+;OJL`Ykan>`Y;;tfyji|TPu?R443n{X; zNWDNF8mn-s>%)${TPb299bi;l7%Ym_II;mo)rDfBR*fSW;F){cUqsb~a?IA@#>8b) zjYEFoTRd?U1ZUB37`V5#-^p78|IeUqKLTrjd`6pWqp9~LbZ5#)lb%`Z@5)~LNGt?`rEp?~^oIeVwcH&S5cp;1}b!Y?3>)jk#`!Hn&FF8|c+g=ow8`Xz$ z4a{3ayxv84!E5|8Wm(n-VObZ4F2FPP%v(uZd2tZ}G>};nzc{12{TUK2R4w$t7jzu~ z_=JaBM;Gi}O+tkxz?d{cU=;0Yx@<3(IH7+IMSy2^&)cF&cM(8PwCZWPlA9|n#G(T{ za52IbjhoAX01~&E#0J>e;~i~faIy*{krrZ+0Y-R1GD#s8xs8PIf@Hi`%!i1$&``j{ z1(*j`2ro$ITnI5H5qOA21=!gt!n7H}Lg`e{Ar=#0KBR$tUI-l%;@nv4MGAsC7maVa`TE~o?EeFuxbO+>KK-Wi3qX_Ib-W|9 zjeHVVfWvS-F9&zc-7AKKwv^87YHY2aA7_p$Oiyo2#zYwghu1TkcWT20!l zhZ}U^gKukK=OJS;3@_vL@N+YtB#0riMcPkl3pI!@E%fy6_AXQE|A_=@ba)Henx_)l z>9|tK+ZMO}-}xP<-X)JUJivdv?xp-IkhsGdeucLHYvTA)iWc1Z$dujZf0Olef3e{$ zbdJ+O)=x;1TgTi&%}qDcfzJ;T#E7EXy_!llqyn2)1ySMU zYPY6s1xds&EEsCVkFw~Q{0>chJP)vQYaBtXGlzG}_g)~$c&NpnC&UOJ@KE*NipSFc z54@tRBSCoEMtV5-dSidAhhs=nCqF8e>fmXBd2mI7aBCWt_H>%rR+8H0LnGBUEAlMB z)89&`B<=Zf)#d6NCcGZbpVor-P-!0el1rWiIFYk_xq!J^OQt*+%5^1Q^w2!2@-2KI zI(X>o_Jo!v0nVSek~O%Jvfo=P{@kCqD_ore)F*!j56jg((LH|&u(WNXu(i&^p_Psy zMJwG@;kpAXvHC_~Ulpx%%<#67iz-}qfKh!LgI|aiPYLHT09@&n)U~=Ddl~TOL3;)t!;9Il#QY)=F0OW%u5aq9~g-ekwtnsB|^g z3)dXrt7ENB68JpdY0c5TNgP_}`01z0!>pPEY-zQUgyDaOa7T6h3o%y3mQqatp8jv? z6e%sPSDiFA`iC7pzVHb_%s2--sBGS93UI=bw{^stN8LnGcj6)f98cwkSv9uM>V6Vv ze8k)5)0fXWPdJ1)GQ+GgZFJQEmU!~2AnI4`Z20J9g7_+TGOOwUD^gtqzPiwVi8h>g znf9JLKoEalO6ii@wp9mM;@)q;g=>GHsNuHx;RBD;ud}}=h`Z8lbX5U%j&%v#`z^TG zAFTlIJ$ry2e&=yKHwmsQ)kaqpVCn5MSF5Plo_(EGe(*G1j9^y*?v-kzt8AfVPM-^+ zQh_(0+(yrz*h~N;6>p;}2AB_TNS!_xaJ9M+;t+rHuxjnm=joM`uMt2g&N1d;Dh603 ztTcr{SWhZ(cpv-EpP7B$w+NsdMKq!lt{h;k#ZI3G`}o313L9R#b9g_#7{1RP{HGMv zR)O<9w@On8d>pc)hE90jLwKPEW+$j^*d>N#16=7KwIpwc?6AYi_n*c#5!5W9hpvOm zx6prDby7(1AnUV`Y$x$n5h^dZqI7!jAIk?=QK;@yv-j+OkaXNS|N2v$t^#+I8oH%h zVTcuu^>0BK&J4d|_IlAegKNXq!6gG+?~}uWaPZ5+qW6PwSs-}9A}f94?Gm)ILV1_C z9cFzg)uAtY2%uh;R@bv>mHwcii{-G8yuN=#^#xBT`}%CEbOqMd6bd1pMsq7Ik<`x% zLPk|lfhFF&u39`OlI9@VXYk6PK~~WKJ9nQ9F|Ksds@=tFmk7WsqpbWPtD*r;s6>)N zZo9ByR@81f0Kw9>RXV^r#O-qd?+Ihg2~nkqoEcTo04oZAfSIJV@U&(;+X%d)28w@d z@d`}zClmpa*20s=O^p2QZ^2J0$^Tn?3O#5{9d=TxFyDNB+W2TcfDL?PCeeJk?}z|9 z*8mH?17)qa0!tZaEt&El0eI!=Xi8ee-boR&`IFZ~oRMyyIEN;Ui~fVyz#oaGA_2Cn z=v3bVKApAUBN=ylTru@g0`Nl4wnl%p(2A@iMG&7umN3v-IC(zJz9m$bS%Xj}ydx{H zt)f?f3)UXpAflCiZswDiNJHItFL-1Hj?e&WIKGhn7e_Ww(0CoZan`d0P_M-BcT8N8 z>@qQq%r??+r!M_3HJdG+@3c&(AWb$Q=3jX7Ah@MS_tyA;?`Ytuw1np=@p^xl+i3t- z%yM@`fD30Vql~B0d{1Tx>tTc#+)`S##Igc2UJpyzT0)4ytr2VB2}@eL3g;}Nj-TeB z%r34Y1n);tw!VyKNOau!;VN>Yq$KKYuM3m#C3p(dK*+d>b<57d_OkZ^<;YJmXL zjiDAo3}J%+58$EJ%J+XE=Ei^U2r*Ox0Um^hTC3iFnxsI8p^_pev07J|-y(aQvJ!s6 zA^+k_|0KY48p=U{i;%Rv#1LXArN};TvNJCGz~%7P5<(23De8^&L5@lgVvx-IY@R9B zLN79Z)kfezh(RjzbJ<%gzzd>$IiVW}G03EQiUqh60Uik<27$6g*1&&v@=s`*U<8C1 zB+*2NM+Eq`rL8IRL%M*VD1;cKP}+KPM1XH!+{y#2PJ)sUVh};mn?+jaDJ|x2x(Lcb zh#_b;(Y6r*p0p%s{z9!Hs0bm3AStppjclPGG68N<|3FX`gcyQi+j?Yx&-^yqH_zbFA9IT=|ZsdyXSUfw-pWW zZAo`?o-aF<=kdgO)wg~^jS#Q3aee~LZTRw_|1|5zV(qQW}&S9p1?D44KW|oc|yj}F_z5htJj=6>W#{0(nZ#(qWVKc3c z&XLR}t)GA5K{LOdDEEOA7AF_r4wc@M7~(clCExktpr=3R9NsP8d%^iVDEx2h_{LoG z0`*QL*2Ri+_DDx&ec{g)9<8GG6Ep;$oqpxyHj;t|K>zxKS3L_G53GJS=hZ;NTfm!&&^yzKmO*Ad|UHYgFhFp#9?0- zYtql3-;wPp{JCs^Cq0zxwMRLFEUbeYL|TXoha);C&#!4;wGdq1=bbE|>XyQvXP84g ziuCHj`j@Fn|G|vzx%x-j;Uoa<+ zn;44grEgG~;qR&VeBe>q-bLVxFjj>y;}@khZ7V1=EPMCxcO@$QH{5UI7CZitwkD3c z@3c&(u&jxXE!g+B!{1jiz|8Ciw~|udC-8p-LhLUdZ1KQO@@0u;`n;Pj;2-|}iY>H5 zX|&NkLp$v+Zl&`RYMD%ci)OH@03YkfWcU&$0$(A-{^E-*nn%f;rkfr0OtJS@)k3og zk0if=Hrjt^r~ShRS$H4(z~=`^7M(fLk=a!2Z>npd8QSQ;pc4CskAO;RrEStBzxjVv z2bkMvQwg#Jfk24;BW*8!bRU&0p4C@ia&BdbpP&FC#Qxw4EM+Zhisn|nfqSXHuPMMy zefCCzKq18bXrJ%{l0cJP_M2T(fb*{eIztdBgt+neE+RqWv(L^BP#HA^I8U!92pmG( zSmOn>(3(`mZ*a{4#-6Yo2ywJEk-dMl(#N={z{D>Nn+QZeh#Mp5Cm}k+tBy>z(#N?9 zurtg;6&3>__TGtrZxCN;cvavkz&y-STJI(h10nWSpjRg;-BW3JRp73LCg*VVRsvBF zVsG#R#V=e7?hcl=)>SW2t3C@o96uRt!jfbfx1E&*XQ%78@`Gmry9F(89UXt~$Skk+ z<6T$aJZvdRgi7q*X7hCma)6bqF;lK9FhdoV%X!@9wM%s0ZLIP+$jK)f%menorF9dI5}Ci#hiUOD(>4?Xc;Pl*XHUvYG#V_#h- zZal5P%q+;01fjv}Vy;M0Ke@%{n;(1c=Yqnkvk9{6-o5cWz*?z=1fjz%_5A&dBiT~( z&`#Qc79LzxbG>gRp02qiwi&!@MdG~nX1%^UywpGlND2Uml6TY;6` zNf3HbsKyuiFAXdWUL#4A8S>UUxEjpU0Hb}54qh8C*#C&rdWJ2?Jgj=yOxOT#2~q^5?f%Sj$}Cw9XAUt?1oZ=soRUk){@c$m28BE2ch5b0^+M%}qB^ zkhs0y@X^aommryGomU@!V6W#F8+#I9ZlAT%I|*WigUrtn;)j9xeW@Z>+})?&65A0B z#s@ZzcI<7)wE}-l%OWljqaDX8Wm8Vdly5p)Wo#TV1~w zIiM;ge_*3=m)m-CK|k7O7=&*iF#GE%=K*2Un*Xe>Hk(7c@9e+Rp)Zfn+onyHa+0cA zO=P!vIdvYc?f|2GhDtcleAb$;L!M-+^Q3`u*MrYr^KE}zxuWy*_{7Ekq0j#RjQRbf zd0(hchOIx=v3DIcQ2hb!p1W0(lJ!JnfS_r$>DU8ruc8L4dkyRy-p~X91Dtj56?+}| zpssyj=kNyBz5v$^46WwopQYV+^;KX@p8#d$i;2 zE((yl3XJv{;?AbSL5G*Q3NYGdh&2=5rmJ?E1gJT{XrCe02EyytL5)gkufS-ZA*OiE zD=ECJ=95@B`wSFA5(ZYq)Kr1dK0_>NwMk-lS#5tUG}>o~pY?uR7U)(@Ei~F^h?7R$ zh|Wt@2iUpuhSq4GA@cYZuv*)Tjz5sekZ{#kU|c!~G2mR!tM%MQ-p9&IRj+|@cmtwO zXA*sSR#?Kzs;am{L{wqDd#pqLdbgOWT4=P-5KX3K*7mHh zhL?X;tiWiWA&T-_MEgW<5mlpd3yt;}B7~>Rn)LE8Y!OwXVhfG-86rcgO*~}|bIUwj z*$RyI8KR(2b&o})>JC=E0^{s6gcwhl&2Q_YR&@s}TY+)*8N!Pv%zAm44rRDeGQen` zA&joK%Mq=(!x>R?1D}pRjq2f06#XGnKMN>LsPy1Iz21j6xv)s;qm=3(jt(smG7$AN2(Fs%+ND$*W|gEvL9+u98cuz>d2i@{TnW^||1ic}ZETw39KtjOLLWET|$LjoK```pOE zQ_y(5Y~?JoC8zI?`|zvKNuknu>w19wYM+A`JW1fR*_u-33(y&d>RhO@r0jpgQF~#Q zsd`8`= z*v=y_lU}#t124Y4WGVAI^_p})f6DL6CG4Kjmne(xtt|Nf*VsNsdGHiQ9P+-g)M6#f zuiTA_t;k9i{SUO=s|T*WvJ>XTH%Gl=`=}S;3?C=JmD=aH4xRv{Ybt%|;LuC$@aha8 iPgCL2K@6S%!sm|zrBkZ;Xqs*S0000 Date: Thu, 2 Feb 2023 16:18:28 +0000 Subject: [PATCH 69/83] Updated headless ui package --- apps/webapp/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 042354710ac..a86a07dfbb8 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -46,7 +46,7 @@ "@codemirror/search": "^6.2.3", "@codemirror/state": "^6.1.3", "@codemirror/view": "^6.5.0", - "@headlessui/react": "^1.6.4", + "@headlessui/react": "^1.7.8", "@heroicons/react": "^2.0.12", "@jsonhero/fetch-hero": "^0.2.2", "@keyv/redis": "^2.3.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47a171ccfd8..9e53dc427ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,7 @@ importers: '@codemirror/state': ^6.1.3 '@codemirror/view': ^6.5.0 '@faker-js/faker': ^7.5.0 - '@headlessui/react': ^1.6.4 + '@headlessui/react': ^1.7.8 '@heroicons/react': ^2.0.12 '@jsonhero/fetch-hero': ^0.2.2 '@keyv/redis': ^2.3.7 @@ -197,7 +197,7 @@ importers: '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 - '@headlessui/react': 1.7.7_biqbaboplfbrettd7655fr4n2y + '@headlessui/react': 1.7.8_biqbaboplfbrettd7655fr4n2y '@heroicons/react': 2.0.13_react@18.2.0 '@jsonhero/fetch-hero': 0.2.2 '@keyv/redis': 2.5.4 @@ -4174,8 +4174,8 @@ packages: '@hapi/hoek': 9.3.0 dev: true - /@headlessui/react/1.7.7_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-BqDOd/tB9u2tA0T3Z0fn18ktw+KbVwMnkxxsGPIH2hzssrQhKB5n/6StZOyvLYP/FsYtvuXfi9I0YowKPv2c1w==} + /@headlessui/react/1.7.8_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-zcwb0kd7L05hxmoAMIioEaOn235Dg0fUO+iGbLPgLVSjzl/l39V6DTpC2Df49PE5aG5/f5q0PZ9ZHZ78ENNV+A==} engines: {node: '>=10'} peerDependencies: react: ^16 || ^17 || ^18 From 910a8d36517d1a0bad0e79ddb6152fb0ab71f592 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 16:18:39 +0000 Subject: [PATCH 70/83] WIP updated onboarding page --- .../app/components/CreateNewWorkflow.tsx | 163 +++++++++--------- apps/webapp/app/components/StyledTabs.tsx | 131 ++++++++++++++ .../orgs/$organizationSlug/__org/index.tsx | 33 ++-- 3 files changed, 229 insertions(+), 98 deletions(-) create mode 100644 apps/webapp/app/components/StyledTabs.tsx diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index ef334371734..c6d0abed083 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -1,4 +1,16 @@ -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; +import { Tab } from "@headlessui/react"; +import { + LargeBox, + LargeBoxList, + Segmented, + SegmentedList, + Underlined, + UnderlinedList, +} from "~/components/StyledTabs"; +import { + ArrowTopRightOnSquareIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import { PopupButton } from "@typeform/embed-react"; import { ApiLogoIcon } from "~/components/code/ApiLogoIcon"; @@ -18,7 +30,7 @@ export function CreateNewWorkflow() { target="_blank" rel="noreferrer" > - + Documentation - + Example Workflows
    @@ -41,86 +53,73 @@ export function CreateNewWorkflowNoWorkflows({ }) { return ( <> - - +
    + + Trigger.dev workflows are written in your own codebase and run in your existing infrastructure. - - To get started - -
      -
    1. - Check out the Quick Start Guide to create your first workflow in - your code. -
    2. -
    3. - Trigger the workflow by writing a test on the Test page. The - workflow run will then appear on the Runs page. -
    4. -
    5. - If you need to authenticate with an API, the Runs page will display - a prompt to connect. -
    6. -
    -
    - - - Quick Start Guide - - - - Example workflows - -
    - - API integrations - - - Easily authenticate with APIs using the supported integrations below. - If there's an integration we don't yet support,{" "} - - vote for it here - {" "} - and we'll add it. - -
    - {providers.map((provider) => ( - - ))} - + - - Fetch - - & - - Webhooks - -
    -
    - Join the community - +
    + Step 1. Install the @trigger.dev package + + + npm + pnpm + yarn + + + + npm install @trigger.dev/sdk + + + pnpm install @trigger.dev/sdk + + + yarn add @trigger.dev/sdk + + + + Step 2. Create your first workflow + + + Start from an example + Start from scratch + + + + + + New user Slack message + Welcome email campaign + + + + New user slack message + + + Welcome email campaign + + + + + + + + Webhook + Custom event + Scheduled (CRON) + + + Webhook + Custom event + Scheduled + + + + + + {/* Join the community + To get help quickly and answers to any questions, join our Discord. @@ -129,10 +128,10 @@ export function CreateNewWorkflowNoWorkflows({ target="_blank" rel="noreferrer" > - Discord + Discord Join Discord - +
    */} ); } diff --git a/apps/webapp/app/components/StyledTabs.tsx b/apps/webapp/app/components/StyledTabs.tsx new file mode 100644 index 00000000000..9417d28b5fe --- /dev/null +++ b/apps/webapp/app/components/StyledTabs.tsx @@ -0,0 +1,131 @@ +import { Tab as HeadlessTab } from "@headlessui/react"; +import classNames from "classnames"; +import classnames from "classnames"; + +type HeadlessTabProps = Parameters[0]; +type HeadlessTabListProps = Parameters[0]; + +export function ClassicList({ children, ...props }: HeadlessTabListProps) { + return ( + + {children} + + ); +} + +export function Classic({ children, ...props }: HeadlessTabProps) { + return ( + + classnames( + selected + ? "border-t border-slate-200 bg-white text-slate-600" + : "border-b border-t border-slate-200 bg-slate-50 text-slate-700 hover:border-slate-200 hover:text-slate-800", + "flex whitespace-nowrap border-r py-3 px-3 text-xs focus:outline-none" + ) + } + {...props} + > + {children} + + ); +} + +export function UnderlinedList({ children, ...props }: HeadlessTabListProps) { + return ( + + {children} + + ); +} + +export function Underlined({ children, ...props }: HeadlessTabProps) { + return ( + + classnames( + selected + ? "border-indigo-500 text-slate-300 outline-none" + : "border-transparent text-slate-300 hover:border-slate-200 hover:text-slate-200", + "disabled:text-slate-300 disabled:hover:border-transparent", + "flex whitespace-nowrap border-b-2 py-2 px-4 text-xs font-medium transition" + ) + } + {...props} + > + {children} + + ); +} + +export function SegmentedList({ + children, + className, + ...props +}: HeadlessTabListProps) { + return ( + + {children} + + ); +} + +export function Segmented({ children, ...props }: HeadlessTabProps) { + return ( + + classnames( + selected + ? "rounded bg-indigo-600 text-white shadow outline-none" + : "rounded text-slate-300 transition hover:bg-slate-700 hover:text-slate-300 hover:shadow-none", + "flex whitespace-nowrap py-2 px-4 text-xs font-medium" + ) + } + {...props} + > + {children} + + ); +} + +export function LargeBoxList({ + children, + className, + ...props +}: HeadlessTabListProps) { + return ( + + {children} + + ); +} + +export function LargeBox({ children, ...props }: HeadlessTabProps) { + return ( + + classnames( + selected + ? "rounded border border-transparent bg-indigo-600 text-white shadow outline-none" + : "rounded border border-slate-700 text-slate-300 transition hover:bg-slate-800 hover:text-slate-300 hover:shadow-none", + "flex whitespace-nowrap py-2 px-4 text-base font-medium" + ) + } + {...props} + > + {children} + + ); +} 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 a49b09575eb..9c54caaa127 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx @@ -55,14 +55,15 @@ export default function Page() { return ( - Workflows {workflows.length === 0 ? ( <> - Create your first workflow + Create your first workflow + ) : ( <> + Workflows {workflows.length} active workflow{workflows.length > 1 ? "s" : ""} @@ -92,30 +93,30 @@ function WorkflowList({ -
    -
    +
    +
    {workflow.status === "CREATED" && ( )} -
    +
    -
    +
    {workflow.title} -
    +
    -
    +
    {workflow.trigger.properties && workflow.trigger.properties.map((property) => (
    -
    -
    +
    +
    Last run: {lastRunDescription(workflow.lastRun)} @@ -151,7 +152,7 @@ function WorkflowList({ {workflow.slug}
    -
    +
    {workflow.integrations.source && (
    @@ -204,7 +205,7 @@ function lastRunDescription(lastRun: WorkflowListItem["lastRun"]) { function PillLabel({ label }: { label: string }) { return ( - + {label} ); @@ -222,7 +223,7 @@ function WorkflowProperty({ {label} - + {content}
    From ab0938e91bb6c43937ada3ad06661cc8ac5327eb Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 17:22:21 +0000 Subject: [PATCH 71/83] Added an external code example --- .../app/components/CreateNewWorkflow.tsx | 32 ++++++++++++++++--- .../samples/new-user-slack-message.ts | 30 +++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 apps/webapp/app/components/samples/new-user-slack-message.ts diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index c6d0abed083..d41e367d5f2 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -12,13 +12,10 @@ import { InformationCircleIcon, } from "@heroicons/react/24/outline"; 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"; -import { Panel } from "./layout/Panel"; import { PrimaryA, SecondaryA } from "./primitives/Buttons"; import { Body } from "./primitives/text/Body"; import { SubTitle } from "./primitives/text/SubTitle"; +import { newUserSlackMessage } from "./samples/new-user-slack-message"; export function CreateNewWorkflow() { return ( @@ -82,6 +79,7 @@ export function CreateNewWorkflowNoWorkflows({ Step 2. Create your first workflow + Start from an example Start from an example Start from scratch @@ -137,3 +135,29 @@ export function CreateNewWorkflowNoWorkflows({ } const allCapsTitleClasses = "mb-2 uppercase tracking-wide text-slate-400"; + +const exampleProjects = [ + { + name: "Send a Slack message when a new user signs up", + requiredPackages: "@trigger.dev/slack zod", + code: newUserSlackMessage, + }, + { + name: "When a new user signs up, send them a series of emails", + requiredPackages: "@trigger.dev/slack zod", + code: "new Trigger() etc...", + }, +]; + +const scratchProjects = [ + { + name: "Send a Slack message when a new user signs up", + requiredPackages: "@trigger.dev/slack zod", + code: "code", + }, + { + name: "When a new user signs up, send them a series of emails", + requiredPackages: "@trigger.dev/slack zod", + code: "new Trigger() etc...", + }, +]; diff --git a/apps/webapp/app/components/samples/new-user-slack-message.ts b/apps/webapp/app/components/samples/new-user-slack-message.ts new file mode 100644 index 00000000000..3f4f851176e --- /dev/null +++ b/apps/webapp/app/components/samples/new-user-slack-message.ts @@ -0,0 +1,30 @@ +export const newUserSlackMessage = `import { Trigger, customEvent } from "@trigger.dev/sdk"; +import { postMessage } from "@trigger.dev/slack"; +import { z } from "zod"; + +new Trigger({ + id: "new-user", + name: "New user slack message", + apiKey: "", + on: customEvent({ + name: "user.created", + schema: z.object({ + name: z.string(), + email: z.string(), + paidPlan: z.boolean(), + }), + }), + run: async (event, ctx) => { + 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 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" + }.\`, + }); + + return response.message; + }, +}).listen();`; From 4077d014e498ffc6f937ff7e189ed1f65f1cecaa Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 17:39:53 +0000 Subject: [PATCH 72/83] Added placeholder pages for more code examples --- apps/webapp/app/components/samples/customEvent.ts | 1 + apps/webapp/app/components/samples/githubStars.ts | 1 + apps/webapp/app/components/samples/scheduled.ts | 1 + apps/webapp/app/components/samples/webhook.ts | 1 + 4 files changed, 4 insertions(+) create mode 100644 apps/webapp/app/components/samples/customEvent.ts create mode 100644 apps/webapp/app/components/samples/githubStars.ts create mode 100644 apps/webapp/app/components/samples/scheduled.ts create mode 100644 apps/webapp/app/components/samples/webhook.ts diff --git a/apps/webapp/app/components/samples/customEvent.ts b/apps/webapp/app/components/samples/customEvent.ts new file mode 100644 index 00000000000..902cad74d29 --- /dev/null +++ b/apps/webapp/app/components/samples/customEvent.ts @@ -0,0 +1 @@ +export const customEvent = `paste code block here`; diff --git a/apps/webapp/app/components/samples/githubStars.ts b/apps/webapp/app/components/samples/githubStars.ts new file mode 100644 index 00000000000..2a463c967c8 --- /dev/null +++ b/apps/webapp/app/components/samples/githubStars.ts @@ -0,0 +1 @@ +export const githubStars = `paste code block here`; diff --git a/apps/webapp/app/components/samples/scheduled.ts b/apps/webapp/app/components/samples/scheduled.ts new file mode 100644 index 00000000000..0c683ee168f --- /dev/null +++ b/apps/webapp/app/components/samples/scheduled.ts @@ -0,0 +1 @@ +export const scheduled = `paste code block here`; diff --git a/apps/webapp/app/components/samples/webhook.ts b/apps/webapp/app/components/samples/webhook.ts new file mode 100644 index 00000000000..801b394391a --- /dev/null +++ b/apps/webapp/app/components/samples/webhook.ts @@ -0,0 +1 @@ +export const webhook = `paste code block here`; From da7742985fd41e367b53c4cbc6a0521fd1fc894b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 17:40:08 +0000 Subject: [PATCH 73/83] Mapping over projects --- apps/webapp/app/components/CreateNewWorkflow.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index d41e367d5f2..ac1df8ffff7 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -16,6 +16,7 @@ import { PrimaryA, SecondaryA } from "./primitives/Buttons"; import { Body } from "./primitives/text/Body"; import { SubTitle } from "./primitives/text/SubTitle"; import { newUserSlackMessage } from "./samples/new-user-slack-message"; +import { link } from "fs"; export function CreateNewWorkflow() { return ( @@ -79,7 +80,6 @@ export function CreateNewWorkflowNoWorkflows({ Step 2. Create your first workflow - Start from an example Start from an example Start from scratch @@ -87,8 +87,9 @@ export function CreateNewWorkflowNoWorkflows({ - New user Slack message - Welcome email campaign + {exampleProjects.map((project) => { + return {project.name}; + })} @@ -143,7 +144,7 @@ const exampleProjects = [ code: newUserSlackMessage, }, { - name: "When a new user signs up, send them a series of emails", + name: "Welcome email campaign", requiredPackages: "@trigger.dev/slack zod", code: "new Trigger() etc...", }, From 8cca0f520b4eb47e747f0eb8e4e7863e30491831 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Thu, 2 Feb 2023 18:08:46 +0000 Subject: [PATCH 74/83] added sample code and renamed files --- .../app/components/samples/custom-event.ts | 14 +++++++++++ .../app/components/samples/customEvent.ts | 1 - .../app/components/samples/github-stars.ts | 24 +++++++++++++++++++ .../app/components/samples/githubStars.ts | 1 - .../app/components/samples/scheduled.ts | 21 +++++++++++++++- apps/webapp/app/components/samples/webhook.ts | 20 +++++++++++++++- 6 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 apps/webapp/app/components/samples/custom-event.ts delete mode 100644 apps/webapp/app/components/samples/customEvent.ts create mode 100644 apps/webapp/app/components/samples/github-stars.ts delete mode 100644 apps/webapp/app/components/samples/githubStars.ts diff --git a/apps/webapp/app/components/samples/custom-event.ts b/apps/webapp/app/components/samples/custom-event.ts new file mode 100644 index 00000000000..27233c140e4 --- /dev/null +++ b/apps/webapp/app/components/samples/custom-event.ts @@ -0,0 +1,14 @@ +export const customEvent = `import { customEvent, Trigger } from "@trigger.dev/sdk"; + +new Trigger({ + id: "user-created-notify-slack", + name: "User Created - Notify Slack", + on: customEvent({ + name: "user.created", + schema: z.object({ id: z.string(), admin: z.boolean() }), + filter: { + admin: [false], + }, + }), + run: async (event, ctx) => {}, +}).listen();`; diff --git a/apps/webapp/app/components/samples/customEvent.ts b/apps/webapp/app/components/samples/customEvent.ts deleted file mode 100644 index 902cad74d29..00000000000 --- a/apps/webapp/app/components/samples/customEvent.ts +++ /dev/null @@ -1 +0,0 @@ -export const customEvent = `paste code block here`; diff --git a/apps/webapp/app/components/samples/github-stars.ts b/apps/webapp/app/components/samples/github-stars.ts new file mode 100644 index 00000000000..76f96f100ba --- /dev/null +++ b/apps/webapp/app/components/samples/github-stars.ts @@ -0,0 +1,24 @@ +export const githubStars = ` +// When a GitHub issue is created or modified, post to Slack + +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", + 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, 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();`; diff --git a/apps/webapp/app/components/samples/githubStars.ts b/apps/webapp/app/components/samples/githubStars.ts deleted file mode 100644 index 2a463c967c8..00000000000 --- a/apps/webapp/app/components/samples/githubStars.ts +++ /dev/null @@ -1 +0,0 @@ -export const githubStars = `paste code block here`; diff --git a/apps/webapp/app/components/samples/scheduled.ts b/apps/webapp/app/components/samples/scheduled.ts index 0c683ee168f..7e923585413 100644 --- a/apps/webapp/app/components/samples/scheduled.ts +++ b/apps/webapp/app/components/samples/scheduled.ts @@ -1 +1,20 @@ -export const scheduled = `paste code block here`; +export const scheduled = `import { scheduleEvent, Trigger } from "@trigger.dev/sdk"; + +new Trigger({ + id: "usage", + name: "usage", + on: scheduleEvent({ rateof: { minutes: 10 } }), + run: async (event, ctx) => { + const { lastRunAt, scheduledTime } = event; + + const query = \`SELECT * FROM users WHERE created_at < \${scheduledTime}\`; + + if (lastRunAt) { + query += \` AND created_at > \${lastRunAt}\`; + } + + const latestUsers = await db.query(query); + + // ... + }, +}).listen();`; diff --git a/apps/webapp/app/components/samples/webhook.ts b/apps/webapp/app/components/samples/webhook.ts index 801b394391a..f63dc6946aa 100644 --- a/apps/webapp/app/components/samples/webhook.ts +++ b/apps/webapp/app/components/samples/webhook.ts @@ -1 +1,19 @@ -export const webhook = `paste code block here`; +export const webhook = `import { webhookEvent, Trigger } from "@trigger.dev/sdk"; + +new Trigger({ + id: "caldotcom-to-slack", + name: "Cal.com To Slack", + on: webhookEvent({ + service: "cal.com", + eventName: "BOOKING_CREATED", + filter: { + triggerEvent: ["BOOKING_CREATED"], + }, + schema: z.any(), + verifyPayload: { + enabled: true, + header: "X-Cal-Signature-256", + }, + }), + run: async (event, ctx) => {}, +}).listen();`; From 3e53699b05813747ab87ce86392ff8ea36bcb924 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Thu, 2 Feb 2023 18:11:50 +0000 Subject: [PATCH 75/83] Added shopify sample --- .../samples/shopify-create-new-product.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 apps/webapp/app/components/samples/shopify-create-new-product.ts diff --git a/apps/webapp/app/components/samples/shopify-create-new-product.ts b/apps/webapp/app/components/samples/shopify-create-new-product.ts new file mode 100644 index 00000000000..a5d888694ff --- /dev/null +++ b/apps/webapp/app/components/samples/shopify-create-new-product.ts @@ -0,0 +1,58 @@ +export const shopifyCreateNewProducts = `import { Trigger, customEvent } from "@trigger.dev/sdk"; +import { z } from "zod"; +import * as shopify from "@trigger.dev/shopify"; + +new Trigger({ + id: "shopify-products", + name: "Shopify products", + apiKey: "", + on: customEvent({ + name: "shopify.products", + schema: z.object({}), + }), + run: async (event, ctx) => { + const newProduct = await shopify.createProduct("create-product", { + descriptionHtml: "This is my brilliant product description.", + title: \`Fantastic product \${Math.floor(Math.random() * 1000)}\`, + productType: "t-shirt", + vendor: "Nike", + options: ["Color", "Size"], + standardizedProductType: { + productTaxonomyNodeId: "gid://shopify/ProductTaxonomyNode/352", + }, + variants: [ + { + price: "99.99", + sku: "variant-1", + inventoryItem: { + tracked: true, + }, + inventoryQuantities: [ + { + availableQuantity: 1, + locationId: "gid://shopify/Location/76187369773", + }, + ], + options: ["Maroon", "Tiny"], + }, + ], + }); + + const newImages = await shopify.appendProductImages("append-images", { + id: newProduct.id, + images: [ + { + src: "https://via.placeholder.com/600/92c952.png", + altText: "Image 1", + }, + { + src: "https://via.placeholder.com/600/d32776.png", + altText: "Image 2", + }, + ], + }); + + return newProduct; + }, +}).listen(); +`; From 6d8100d2005d6c6e14bf05e32c6a295830b339d1 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Thu, 2 Feb 2023 18:18:53 +0000 Subject: [PATCH 76/83] Added whatsapp sample --- .../whatsapp-listen-for-message-and-reply.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 apps/webapp/app/components/samples/whatsapp-listen-for-message-and-reply.ts diff --git a/apps/webapp/app/components/samples/whatsapp-listen-for-message-and-reply.ts b/apps/webapp/app/components/samples/whatsapp-listen-for-message-and-reply.ts new file mode 100644 index 00000000000..36f049a4c84 --- /dev/null +++ b/apps/webapp/app/components/samples/whatsapp-listen-for-message-and-reply.ts @@ -0,0 +1,38 @@ +export const whatsappListenForMessageAndReply = ` +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(); +`; + + From 81d3d324e2c6c0d080dcbd0d0bac577ed8cb42d5 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 18:41:12 +0000 Subject: [PATCH 77/83] More progress on the onboarding --- .../app/components/CreateNewWorkflow.tsx | 106 ++++++++++++------ apps/webapp/app/components/StyledTabs.tsx | 6 +- .../components/primitives/text/SubTitle.tsx | 10 +- 3 files changed, 80 insertions(+), 42 deletions(-) diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index ac1df8ffff7..7539e926247 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -2,21 +2,18 @@ import { Tab } from "@headlessui/react"; import { LargeBox, LargeBoxList, - Segmented, - SegmentedList, Underlined, UnderlinedList, } from "~/components/StyledTabs"; -import { - ArrowTopRightOnSquareIcon, - InformationCircleIcon, -} from "@heroicons/react/24/outline"; +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import type { IntegrationMetadata } from "@trigger.dev/integration-sdk"; import { PrimaryA, SecondaryA } from "./primitives/Buttons"; import { Body } from "./primitives/text/Body"; import { SubTitle } from "./primitives/text/SubTitle"; import { newUserSlackMessage } from "./samples/new-user-slack-message"; -import { link } from "fs"; +import CodeBlock from "./code/CodeBlock"; +import { Header4 } from "./primitives/text/Headers"; +import classNames from "classnames"; export function CreateNewWorkflow() { return ( @@ -51,33 +48,49 @@ export function CreateNewWorkflowNoWorkflows({ }) { return ( <> -
    + {/*
    Trigger.dev workflows are written in your own codebase and run in your existing infrastructure. -
    - Step 1. Install the @trigger.dev package +
    */} + + 1. Install the package + - - npm - pnpm - yarn - - + + npm + pnpm + yarn + + - npm install @trigger.dev/sdk + - pnpm install @trigger.dev/sdk + - yarn add @trigger.dev/sdk + - Step 2. Create your first workflow + + 2. Create your workflow + Start from an example @@ -85,33 +98,46 @@ export function CreateNewWorkflowNoWorkflows({ + {/* Example projects titles */} {exampleProjects.map((project) => { return {project.name}; })} + {/* Example projects content */} - - New user slack message - - - Welcome email campaign - + {exampleProjects.map((project) => { + return ( + + + Install the extra API integration packages + + + + + ); + })} + {/* From scratch projects titles */} - Webhook - Custom event - Scheduled (CRON) + {fromScratchProjects.map((project) => { + return {project.name}; + })} + {/* From scratch projects content */} - Webhook - Custom event - Scheduled + {fromScratchProjects.map((project) => { + return ( + + + + ); + })} @@ -135,29 +161,35 @@ export function CreateNewWorkflowNoWorkflows({ ); } +const subTitle = "text-slate-200 font-semibold mb-4"; const allCapsTitleClasses = "mb-2 uppercase tracking-wide text-slate-400"; const exampleProjects = [ { - name: "Send a Slack message when a new user signs up", + name: "New user signs up → Post Slack message", requiredPackages: "@trigger.dev/slack zod", code: newUserSlackMessage, }, { - name: "Welcome email campaign", + name: "New user signs up → send email campaign", requiredPackages: "@trigger.dev/slack zod", code: "new Trigger() etc...", }, ]; -const scratchProjects = [ +const fromScratchProjects = [ { - name: "Send a Slack message when a new user signs up", + name: "Webhook", requiredPackages: "@trigger.dev/slack zod", code: "code", }, { - name: "When a new user signs up, send them a series of emails", + name: "Custom event", + requiredPackages: "@trigger.dev/slack zod", + code: "new Trigger() etc...", + }, + { + name: "Scheduled (CRON)", requiredPackages: "@trigger.dev/slack zod", code: "new Trigger() etc...", }, diff --git a/apps/webapp/app/components/StyledTabs.tsx b/apps/webapp/app/components/StyledTabs.tsx index 9417d28b5fe..c12cd3cc872 100644 --- a/apps/webapp/app/components/StyledTabs.tsx +++ b/apps/webapp/app/components/StyledTabs.tsx @@ -34,7 +34,7 @@ export function Classic({ children, ...props }: HeadlessTabProps) { export function UnderlinedList({ children, ...props }: HeadlessTabListProps) { return ( {children} @@ -49,9 +49,9 @@ export function Underlined({ children, ...props }: HeadlessTabProps) { classnames( selected ? "border-indigo-500 text-slate-300 outline-none" - : "border-transparent text-slate-300 hover:border-slate-200 hover:text-slate-200", + : "border-transparent text-slate-500 hover:border-slate-200 hover:text-slate-200", "disabled:text-slate-300 disabled:hover:border-transparent", - "flex whitespace-nowrap border-b-2 py-2 px-4 text-xs font-medium transition" + "flex whitespace-nowrap border-b-2 py-2 px-4 text-base font-medium transition" ) } {...props} diff --git a/apps/webapp/app/components/primitives/text/SubTitle.tsx b/apps/webapp/app/components/primitives/text/SubTitle.tsx index a9292ac48db..7a6679bce80 100644 --- a/apps/webapp/app/components/primitives/text/SubTitle.tsx +++ b/apps/webapp/app/components/primitives/text/SubTitle.tsx @@ -1,8 +1,14 @@ import { Header2 } from "./Headers"; -export function SubTitle({ children }: { children: React.ReactNode }) { +export function SubTitle({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { return ( - + {children} ); From 2b4a74f6726070a02ac30459ae15342e95a52c62 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 19:10:22 +0000 Subject: [PATCH 78/83] WIP adding api keys to the code samples --- .../app/components/CreateNewWorkflow.tsx | 42 ++++++++++++++----- .../app/components/samples/github-stars.ts | 8 ++-- .../samples/new-user-slack-message.ts | 6 ++- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 7539e926247..4e00045ecdb 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -14,6 +14,8 @@ import { newUserSlackMessage } from "./samples/new-user-slack-message"; import CodeBlock from "./code/CodeBlock"; import { Header4 } from "./primitives/text/Headers"; import classNames from "classnames"; +import { useCurrentEnvironment } from "~/hooks/useEnvironments"; +import invariant from "tiny-invariant"; export function CreateNewWorkflow() { return ( @@ -46,6 +48,8 @@ export function CreateNewWorkflowNoWorkflows({ }: { providers: IntegrationMetadata[]; }) { + const environment = useCurrentEnvironment(); + invariant(environment, "Environment must be defined"); return ( <> {/*
    @@ -111,10 +115,19 @@ export function CreateNewWorkflowNoWorkflows({ return ( - Install the extra API integration packages + Install these extra API integration packages - - + + {project.description} + ); })} @@ -134,7 +147,10 @@ export function CreateNewWorkflowNoWorkflows({ {fromScratchProjects.map((project) => { return ( - + ); })} @@ -166,14 +182,17 @@ const allCapsTitleClasses = "mb-2 uppercase tracking-wide text-slate-400"; const exampleProjects = [ { - name: "New user signs up → Post Slack message", - requiredPackages: "@trigger.dev/slack zod", + name: "New GitHub star recieved → Post details to Slack", + requiredPackages: "@trigger.dev/slack @trigger.dev/github zod", code: newUserSlackMessage, + description: + "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.", }, { name: "New user signs up → send email campaign", requiredPackages: "@trigger.dev/slack zod", - code: "new Trigger() etc...", + code: newUserSlackMessage, + description: "", }, ]; @@ -181,16 +200,19 @@ const fromScratchProjects = [ { name: "Webhook", requiredPackages: "@trigger.dev/slack zod", - code: "code", + code: newUserSlackMessage, + description: "", }, { name: "Custom event", requiredPackages: "@trigger.dev/slack zod", - code: "new Trigger() etc...", + code: newUserSlackMessage, + description: "", }, { name: "Scheduled (CRON)", requiredPackages: "@trigger.dev/slack zod", - code: "new Trigger() etc...", + code: newUserSlackMessage, + description: "", }, ]; diff --git a/apps/webapp/app/components/samples/github-stars.ts b/apps/webapp/app/components/samples/github-stars.ts index 76f96f100ba..076cd3c8bab 100644 --- a/apps/webapp/app/components/samples/github-stars.ts +++ b/apps/webapp/app/components/samples/github-stars.ts @@ -1,6 +1,5 @@ -export const githubStars = ` -// When a GitHub issue is created or modified, post to Slack - +export function githubStars(apiKey: string) { + return `// When a GitHub issue is created or modified, post to Slack import { Trigger } from "@trigger.dev/sdk"; import * as github from "@trigger.dev/github"; import * as slack from "@trigger.dev/slack"; @@ -8,7 +7,7 @@ import * as slack from "@trigger.dev/slack"; new Trigger({ id: "my-workflow-1", name: "Posts to Slack when GitHub Issue created or modified", - apiKey: "", + apiKey: "${apiKey}", on: github.events.issueEvent({ repo: "my-github-org/my-github-repo", }), @@ -22,3 +21,4 @@ new Trigger({ return response.message; }, }).listen();`; +} diff --git a/apps/webapp/app/components/samples/new-user-slack-message.ts b/apps/webapp/app/components/samples/new-user-slack-message.ts index 3f4f851176e..2e7e48c0c00 100644 --- a/apps/webapp/app/components/samples/new-user-slack-message.ts +++ b/apps/webapp/app/components/samples/new-user-slack-message.ts @@ -1,11 +1,12 @@ -export const newUserSlackMessage = `import { Trigger, customEvent } from "@trigger.dev/sdk"; +export function newUserSlackMessage(apiKey: string) { + return `import { Trigger, customEvent } from "@trigger.dev/sdk"; import { postMessage } from "@trigger.dev/slack"; import { z } from "zod"; new Trigger({ id: "new-user", name: "New user slack message", - apiKey: "", + apiKey: "${apiKey}", on: customEvent({ name: "user.created", schema: z.object({ @@ -28,3 +29,4 @@ new Trigger({ return response.message; }, }).listen();`; +} From 93903bf072f9d2574a6fc216d35d3d09a85b2f10 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 19:38:09 +0000 Subject: [PATCH 79/83] Moved the env variables up to the workspace --- .../app/components/CreateNewWorkflow.tsx | 90 ++++++++++--------- apps/webapp/app/hooks/useEnvironments.ts | 4 +- .../routes/__app/orgs/$organizationSlug.tsx | 7 +- .../workflows/$workflowSlug.tsx | 1 - 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx index 4e00045ecdb..dbdc0b884bc 100644 --- a/apps/webapp/app/components/CreateNewWorkflow.tsx +++ b/apps/webapp/app/components/CreateNewWorkflow.tsx @@ -60,38 +60,9 @@ export function CreateNewWorkflowNoWorkflows({
    */} - 1. Install the package + 1. Install the Trigger.dev package - - - npm - pnpm - yarn - - - - - - - - - - - - - + 2. Create your workflow @@ -114,16 +85,13 @@ export function CreateNewWorkflowNoWorkflows({ {exampleProjects.map((project) => { return ( - - Install these extra API integration packages + + {project.description} - - {project.description} + + Install these extra API integration packages: + + + + npm + pnpm + yarn + + + + + + + + + + + + +
    + ); +} diff --git a/apps/webapp/app/hooks/useEnvironments.ts b/apps/webapp/app/hooks/useEnvironments.ts index fdb4790b853..dc09c3687fc 100644 --- a/apps/webapp/app/hooks/useEnvironments.ts +++ b/apps/webapp/app/hooks/useEnvironments.ts @@ -36,9 +36,7 @@ export function useEnvironments(): RuntimeEnvironment[] | undefined { export function useCurrentEnvironment(): RuntimeEnvironment | undefined { const environments = useEnvironments(); - const routeMatch = useMatchesData( - "routes/__app/orgs/$organizationSlug/workflows/$workflowSlug" - ); + const routeMatch = useMatchesData("routes/__app/orgs/$organizationSlug"); const currentEnvironmentSlug = routeMatch?.data.currentEnvironmentSlug; if (!currentEnvironmentSlug || !environments) { diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx index 94cc29010ac..1fbfdce5e27 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx @@ -4,6 +4,7 @@ import { requireUserId } from "~/services/session.server"; import { Outlet } from "@remix-run/react"; import { getOrganizationFromSlug } from "~/models/organization.server"; import { typedjson } from "remix-typedjson"; +import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server"; export const loader = async ({ request, params }: LoaderArgs) => { const userId = await requireUserId(request); @@ -19,7 +20,11 @@ export const loader = async ({ request, params }: LoaderArgs) => { throw new Response("Not Found", { status: 404 }); } - return typedjson({ organization }); + const currentEnvironmentSlug = await getRuntimeEnvironmentFromRequest( + request + ); + + return typedjson({ organization, currentEnvironmentSlug }); }; export default function Organization() { diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx index d7b62f931f5..c20526f4aff 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx @@ -157,7 +157,6 @@ export const loader = async ({ request, params }: LoaderArgs) => { rules, externalSourceConfig, }, - currentEnvironmentSlug, connectionSlots, }); }; From e77bfc77e0f70cbad85f92be89165fbf7e95eaa4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 2 Feb 2023 19:38:17 +0000 Subject: [PATCH 80/83] more style improvements --- apps/webapp/app/components/StyledTabs.tsx | 2 +- apps/webapp/app/components/code/CodeBlock.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/components/StyledTabs.tsx b/apps/webapp/app/components/StyledTabs.tsx index c12cd3cc872..bd8d75453a9 100644 --- a/apps/webapp/app/components/StyledTabs.tsx +++ b/apps/webapp/app/components/StyledTabs.tsx @@ -34,7 +34,7 @@ export function Classic({ children, ...props }: HeadlessTabProps) { export function UnderlinedList({ children, ...props }: HeadlessTabListProps) { return ( {children} diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index e52721afdea..03bb0bb3b00 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import Prism from "prismjs"; import "prismjs/components/prism-typescript"; import "prismjs/components/prism-json"; +import "prismjs/components/prism-bash"; import "prismjs/plugins/line-numbers/prism-line-numbers"; import "prismjs/plugins/line-numbers/prism-line-numbers.css"; import { CopyTextButton } from "../CopyTextButton"; @@ -11,7 +12,7 @@ Prism.manual = true; type CodeBlockProps = { code: string; - language?: "typescript" | "json"; + language?: "typescript" | "json" | "bash"; showCopyButton?: boolean; align?: "top" | "center"; maxHeight?: string; @@ -60,7 +61,7 @@ export default function CodeBlock({ {showCopyButton === true && ( )} {maxHeight && ( -
    +