diff --git a/apps/docs/examples/examples.mdx b/apps/docs/examples/examples.mdx
index c8196f5e109..e1addc36d8c 100644
--- a/apps/docs/examples/examples.mdx
+++ b/apps/docs/examples/examples.mdx
@@ -17,5 +17,7 @@ You can use these workflows in your product by following the instructions on eac
Post to Slack when a GitHub issue is created or modified.
-
+
+ Create a welcome email drip campaign.
+
diff --git a/apps/docs/examples/resend.mdx b/apps/docs/examples/resend.mdx
index 8e3d744d306..36ce42ac255 100644
--- a/apps/docs/examples/resend.mdx
+++ b/apps/docs/examples/resend.mdx
@@ -4,6 +4,11 @@ sidebarTitle: "Resend"
description: "Here are some example workflows you could create using the Resend integration."
---
+
+ Resend is in private beta, if you would like access please contact us at{" "}
+ help@trigger.dev
+
+
## Welcome email drip campaign
This workflow will get triggered and a Slack notification and welcom email will be sent straight away.
diff --git a/apps/docs/functions/send-event.mdx b/apps/docs/functions/send-event.mdx
index b709fcaaae7..9c11909a576 100644
--- a/apps/docs/functions/send-event.mdx
+++ b/apps/docs/functions/send-event.mdx
@@ -4,9 +4,12 @@ sidebarTitle: "Send event"
description: "Send events to trigger your custom events workflows."
---
-## How to send an event
+## Send an event
-You can send an event from anywhere in your code, including from inside another workflow.
+You can easily send an event from anywhere, including from inside another workflow. Events don't have to come from the same server as your workflow and can be sent as HTTP requests from any language (see our cURL example below for the format).
+
+
+
```ts
import { sendEvent } from "@trigger.dev/sdk";
@@ -19,12 +22,180 @@ await sendEvent("start-fire-2", {
});
```
+
+
+
+```bash
+curl --request POST \
+ --url https://app.trigger.dev/api/v1/events \
+ --header 'Authorization: Bearer ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "id": "",
+ "event": {
+ "name": "user.created",
+ "payload": {
+ "userId": "123456"
+ }
+ }
+ }'
+```
+
+
+
+
If you are calling this from inside a workflow, ensure that the first parameter is unique inside your workflow.
## Sending events from other workflows
-It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them.
+It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them. The below example shows how a scheduled workflow delegates the work via sending a custom event:
+
+
+
+```ts check-scheduler.ts
+new Trigger({
+ id: "check-scheduler",
+ name: "Check Scheduler",
+ on: scheduleEvent({ rateOf: { minutes: 10 } }),
+ run: async (event, context) => {
+ await context.sendEvent("health.check trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://trigger.dev",
+ host: "trigger.dev",
+ },
+ });
+
+ await context.sendEvent("health.check docs.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://docs.trigger.dev",
+ host: "docs.trigger.dev",
+ },
+ });
+
+ await context.sendEvent("health.check app.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://app.trigger.dev/healthcheck",
+ host: "app.trigger.dev",
+ },
+ });
+ },
+}).listen();
+```
+
+```ts health-check.ts
+export const healthCheck = new Trigger({
+ id: "health-check",
+ name: "Health Check",
+ on: customEvent({
+ name: "health.check",
+ schema: z.object({
+ url: z.string().url(),
+ host: z.string(),
+ }),
+ }),
+ run: async (event, context) => {
+ const response = await context.fetch("fetch site", event.url, {
+ method: "GET",
+ retry: {
+ enabled: false,
+ },
+ });
+
+ if (response.ok) {
+ await context.logger.info(`${event.host} is up!`);
+ return;
+ }
+
+ await slack.postMessage("Site is down", {
+ channelName: "health-checks",
+ text: `${event.host} is down: ${response.status}`,
+ });
+ },
+}).listen();
+```
+
+
+
+## Delaying event delivery
-## Writing a workflow that is triggered by an event
+Through both our Node.js SDK and our HTTP API, you can delay the delivery of an event. This is useful if you want to send an event to trigger a workflow, but you want to wait for a certain amount of time before the event is delivered.
-You should read [the documentation on custom events](/triggers/custom-events) to learn how to write a workflow that is triggered by an event.
+Using the health check example above, we can delay the delivery of the `health.check` event in various ways:
+
+```ts
+new Trigger({
+ id: "check-scheduler",
+ name: "Check Scheduler",
+ on: scheduleEvent({ rateOf: { minutes: 10 } }),
+ run: async (event, context) => {
+ await context.sendEvent("health.check trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://trigger.dev",
+ host: "trigger.dev",
+ },
+ delay: { until: new Date(Date.now() + 1000 * 60 * 5) }, // Delay until a specific date
+ });
+
+ await context.sendEvent("health.check docs.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://docs.trigger.dev",
+ host: "docs.trigger.dev",
+ },
+ delay: { minutes: 5 }, // Delay for a specific amount of time
+ });
+
+ await context.sendEvent("health.check app.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://app.trigger.dev/healthcheck",
+ host: "app.trigger.dev",
+ },
+ delay: { seconds: 30 }, // Delay for a specific amount of time
+ });
+ },
+}).listen();
+```
+
+
+ When sending events in the context of a workflow run, adding a delay DOES NOT
+ add a delay to the workflow run, as sending a custom event is a
+ fire-and-forget action.
+
+
+You can add delays via the HTTP API by adding a `delay` object to the event:
+
+```bash
+curl --request POST \
+ --url https://app.trigger.dev/api/v1/events \
+ --header 'Authorization: Bearer ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "id": "",
+ "event": {
+ "name": "user.created",
+ "payload": {
+ "userId": "123456"
+ },
+ "delay": {
+ "seconds": 30
+ }
+ }
+ }'
+```
+
+## Event deduplication
+
+When sending events, you can specify an `id` for the event. If you send an event with the same `id` multiple times, only the first event will be delivered. This is useful if you want to ensure that an event is only delivered once, even if you send it multiple times.
+
+```ts
+sendEvent({
+ id: "my-event-id",
+ name: "my-event",
+ payload: { foo: "bar" },
+});
+```
diff --git a/apps/docs/integrations/apis/slack/actions/post-message.mdx b/apps/docs/integrations/apis/slack/actions/post-message.mdx
index e1e9585a583..e2689e35f05 100644
--- a/apps/docs/integrations/apis/slack/actions/post-message.mdx
+++ b/apps/docs/integrations/apis/slack/actions/post-message.mdx
@@ -28,6 +28,14 @@ Publish slack messages to a public or private channel in your Slack Workspace as
The formatted text of the message to be published, formatted as [mrkdwn](https://api.slack.com/reference/surfaces/formatting#basics).
+
+ You can use blocks to create a rich message with images, buttons, and more.
+
+ You can either pass in a JavaScript object or use [JSX Slack](https://github.com/yhatt/jsx-slack/) to create blocks. We highly recommend using JSX Slack, it's much easier to read and write. See the [block example](#) to see it in action.
+
+ Full full detail on block see the [Slack API docs](https://api.slack.com/reference/block-kit/blocks).
+
+
@@ -69,6 +77,8 @@ Publish slack messages to a public or private channel in your Slack Workspace as
## Example Workflows
+### Notify Slack on New Star
+
```typescript Notify Slack on New Star
@@ -89,11 +99,7 @@ new Trigger({
}).listen();
```
-
-
-## Example Response
-
-```json
+```json Example response
{
"ok": true,
"ts": "1673618429.084699",
@@ -107,3 +113,135 @@ new Trigger({
}
}
```
+
+
+
+### Blocks with interactivity
+
+You can add interaction and richer visual elements using Blocks. The easiest way to add them is using `jsx-slack` (which is a separate npm package that you can install).
+
+This example messages your team every minute (!) and asks them how they're doing. There are two buttons and a dropdown menu.
+
+In this examples there are two workflows:
+
+1. Messages your team every minute (!) and asks them how they're doing.
+2. Receives the interaction from users. It adds reactions when the buttons are pressed and posts a message when a user selects from the dropdown menu.
+
+
+
+```typescript Blocks with interactivity
+import { slack } from "@trigger.dev/integrations";
+import JSXSlack, {
+ Actions,
+ Blocks,
+ Button,
+ Section,
+ Select,
+ Option,
+} from "jsx-slack";
+import { z } from "zod";
+
+const BLOCK_ID = "issue.action.block";
+
+//1. every minute see how your employees are doing, we don't recommend this frequency 😉
+new Trigger({
+ id: "slack-interactivity",
+ name: "Testing Slack Interactivity",
+ on: scheduleEvent({
+ rateOf: {
+ minutes: 1,
+ },
+ }),
+ run: async (event, ctx) => {
+ await slack.postMessage("jsx-test", {
+ channelName: "test-integrations",
+ //text appears in Slack notifications on mobile/desktop
+ text: "How is your progress today?",
+ //import and use JSXSlack to make creating rich messages much easier
+ blocks: JSXSlack(
+
+ How is your progress today?
+
+
+ I'm blocked
+
+
+ Get help
+
+
+ 5 {":star:".repeat(5)}
+ 4 {":star:".repeat(4)}
+ 3 {":star:".repeat(3)}
+ 2 {":star:".repeat(2)}
+ 1 {":star:".repeat(1)}
+
+
+
+ ),
+ });
+ },
+}).listen();
+
+//2. this workflow listens for Slack interactions filtered by the block id and actions we used above
+new Trigger({
+ id: "slack-block-interaction",
+ name: "Slack Block Interaction",
+ on: slack.events.blockActionInteraction({
+ blockId: BLOCK_ID,
+ actionId: ["status-blocked", "status-help", "rating"],
+ }),
+ run: async (event, ctx) => {
+ //create promises from all the actions
+ const promises = event.actions.map((action) => {
+ switch (action.action_id) {
+ case "status-blocked": {
+ //the user is blocked so add a 😢 emoji as a reaction
+ if (event.message) {
+ return slack.addReaction("React to message", {
+ name: "cry",
+ timestamp: event.message.ts,
+ channelId: event.channel.id,
+ });
+ }
+ }
+ case "status-help": {
+ //the user needs help so add an 🆘 emoji as a reaction
+ if (event.message) {
+ return slack.addReaction("React to message", {
+ name: "sos",
+ timestamp: event.message.ts,
+ channelId: event.channel.id,
+ });
+ }
+ }
+ case "rating": {
+ if (action.type != "static_select") {
+ throw new Error("This action should be a select");
+ }
+
+ //post the rating as a message that appears below the original,
+ //only the user pressing the button will see this message
+ return slack.postMessageResponse(
+ "Added a comment to the issue",
+ event.response_url,
+ {
+ text: `You rated your day ${action.selected_option?.value} stars`,
+ replace_original: false,
+ }
+ );
+ }
+ default:
+ return Promise.resolve();
+ }
+ });
+
+ await Promise.all(promises);
+ },
+}).listen();
+```
+
+
diff --git a/apps/docs/mint.json b/apps/docs/mint.json
index 41e403a370c..77f128cced6 100644
--- a/apps/docs/mint.json
+++ b/apps/docs/mint.json
@@ -3,7 +3,7 @@
"logo": {
"dark": "/logo/light.png",
"light": "/logo/light.png",
- "href": "https://trigger.dev"
+ "href": "https://app.trigger.dev"
},
"favicon": "/images/favicon.png",
"colors": {
@@ -20,6 +20,16 @@
"default": "dark",
"isHidden": true
},
+ "topbarLinks": [
+ {
+ "name": "Login",
+ "url": "https://app.trigger.dev"
+ },
+ {
+ "name": "Sign up",
+ "url": "https://app.trigger.dev"
+ }
+ ],
"topbarCtaButton": {
"type": "github",
"url": "https://github.com/triggerdotdev/trigger.dev"
@@ -39,11 +49,7 @@
"navigation": [
{
"group": "Getting Started",
- "pages": [
- "welcome",
- "getting-started",
- "get-help"
- ]
+ "pages": ["welcome", "getting-started", "get-help"]
},
{
"group": "Examples",
@@ -71,15 +77,11 @@
"pages": [
{
"group": "Slack",
- "pages": [
- "integrations/apis/slack/actions/post-message"
- ]
+ "pages": ["integrations/apis/slack/actions/post-message"]
},
{
"group": "Resend.com",
- "pages": [
- "integrations/apis/resend/actions/send-email"
- ]
+ "pages": ["integrations/apis/resend/actions/send-email"]
}
]
},
@@ -111,7 +113,8 @@
"reference/trigger",
"reference/custom-event",
"reference/webhook-event",
- "reference/schedule-event"
+ "reference/schedule-event",
+ "reference/send-event"
]
},
{
@@ -137,4 +140,4 @@
"github": "https://github.com/triggerdotdev/trigger.dev",
"discord": "https://discord.gg/nkqV9xBYWy"
}
-}
\ No newline at end of file
+}
diff --git a/apps/docs/reference/send-event.mdx b/apps/docs/reference/send-event.mdx
new file mode 100644
index 00000000000..06c931d9f0f
--- /dev/null
+++ b/apps/docs/reference/send-event.mdx
@@ -0,0 +1,97 @@
+---
+title: "sendEvent"
+sidebarTitle: "sendEvent"
+description: "Send a custom event to Trigger, from inside or outside of a workflow."
+---
+
+## Usage
+
+```ts
+import { Trigger, customEvent } from "@trigger.dev/sdk";
+
+new Trigger({
+ id: "usage",
+ name: "usage",
+ on: customEvent({
+ name: "user.created",
+ schema: z.any(),
+ }),
+ run: async (event, ctx) => {
+ await ctx.sendEvent("Sending context user.created event", {
+ name: "user.created",
+ payload: {
+ id: "1234_abc",
+ },
+ });
+ },
+}).listen();
+```
+
+You can also use `sendEvent` by importing it from `@trigger.dev/sdk`:
+
+```ts
+import { sendEvent } from "@trigger.dev/sdk";
+
+await sendEvent("Sending context user.created event", {
+ name: "user.created",
+ payload: {
+ id: "1234_abc",
+ },
+});
+```
+
+
+ If calling `sendEvent` from outside of a workflow run, it will make an HTTP
+ API request to app.trigger.dev to send the event. Make sure you set the
+ `TRIGGER_API_KEY` environment variable if you will be using it this way.
+
+
+## Parameters
+
+
+ A unique key for the event in the context of a workflow run. Please see the
+ [Keys and Resumability](/guides/resumability) guide for more info.
+
+
+
+
+
+ An optional unique ID for the event. If not provided, one will be
+ generated automatically (using `clid`). Set this field to perform event
+ deduplication.
+
+
+
+ The name of the event. This is the name you set when creating the
+ `customEvent` trigger.
+
+
+
+ The payload of the event.
+
+
+
+ An optional timestamp for the event. If not provided, one will be generated. Must be in ISO 8601 format (e.g. `new Date().toISOString()`)
+
+
+
+ An optional context object for the event. This can be used to pass
+ additional information about the event, such as the user who triggered
+ it. Note that this is not currently exposed to the workflow (coming soon).
+
+
+
+ An optional delay object for the event. This can be used to delay the
+ event by a certain amount of time. Use one of the following options:
+
+ - `{ seconds: number }` - delay the event by a certain number of seconds
+ - `{ minutes: number }` - delay the event by a certain number of minutes
+ - `{ hours: number }` - delay the event by a certain number of hours
+ - `{ days: number }` - delay the event by a certain number of days
+ - `{ until: Date }` - delay the event until a certain date
+
+ See the [Delaying Event Delivery](/functions/send-event#delaying-event-delivery) docs for more info.
+
+
+
+
diff --git a/apps/docs/welcome.mdx b/apps/docs/welcome.mdx
index 98c6b3043e9..c723b28780f 100644
--- a/apps/docs/welcome.mdx
+++ b/apps/docs/welcome.mdx
@@ -8,19 +8,21 @@ Trigger.dev is an open source platform that enables developers to create event-d
Workflows live in your codebase so you can use your existing types, functions, version control and IDE. They are triggered by us but run on your server so your private data is never exposed.
+Go to [trigger.dev](https://app.trigger.dev) and sign up or login to your account.
+
_A workflow in action:_

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