&%^7Of;6cmuZnV61@f
z6~KK}_%ia!`trx<>d|cz>jwhD7k{h~{0BVqBjYJ=o$H$iYqUewiy^_*eH0e|`G<|t
z2Sw%0^q;KW{I_jv{wqMp?0m{AAlr%j0^v6R`GF+*k
zyOMk6q(B!TwWmA8f$3ABRb~1d?_Lq!GxIF|p5D0LbScQnNmI974WGuPDL2Em(Cf^{
zbwDRNaA`y{uARd2Y(Y~Um1N%4J%Pll5}vQ!r3C^ok&8F
bu0QtqqM0poQ*t)}J;C7V>gTe~DWM4f7P>|v
diff --git a/apps/webapp/app/components/integrations/ApiKeyHelp.tsx b/apps/webapp/app/components/integrations/ApiKeyHelp.tsx
deleted file mode 100644
index dd0d851389..0000000000
--- a/apps/webapp/app/components/integrations/ApiKeyHelp.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Help, Integration } from "~/services/externalApis/types";
-import { InlineCode } from "../code/InlineCode";
-import { Header1 } from "../primitives/Headers";
-import { Paragraph } from "../primitives/Paragraph";
-import { HelpInstall } from "./HelpInstall";
-import { HelpSamples, ReplacementData } from "./HelpSamples";
-
-export type HelpPanelIntegration = Pick;
-
-export type HelpPanelProps = {
- integration: HelpPanelIntegration;
- help?: Help;
- integrationClient?: ReplacementData;
-};
-
-export function ApiKeyHelp({ integration, help, integrationClient }: HelpPanelProps) {
- return (
-
-
How to use {integration.name} with API keys
-
- You can use API keys to authenticate with {integration.name}. Your API keys won't leave your
- server, we'll never see them.
-
-
- First install the {integration.packageName} package using your
- preferred package manager. For example:
-
-
- {help && (
-
- )}
-
- );
-}
diff --git a/apps/webapp/app/components/integrations/ConnectToIntegrationSheet.tsx b/apps/webapp/app/components/integrations/ConnectToIntegrationSheet.tsx
deleted file mode 100644
index 37c80df5e6..0000000000
--- a/apps/webapp/app/components/integrations/ConnectToIntegrationSheet.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { useState } from "react";
-import { Integration } from "~/services/externalApis/types";
-import { apiReferencePath, docsIntegrationPath } from "~/utils/pathBuilder";
-import { LinkButton } from "../primitives/Buttons";
-import { Header1, Header2 } from "../primitives/Headers";
-import { NamedIconInBox } from "../primitives/NamedIcon";
-import { Paragraph } from "../primitives/Paragraph";
-import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
-import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
-import { ApiKeyHelp } from "./ApiKeyHelp";
-import { CustomHelp } from "./CustomHelp";
-import { SelectOAuthMethod } from "./SelectOAuthMethod";
-
-type IntegrationMethod = "apikey" | "oauth2" | "custom";
-
-export function ConnectToIntegrationSheet({
- integration,
- organizationId,
- button,
- className,
- callbackUrl,
- icon,
-}: {
- integration: Integration;
- organizationId: string;
- button: React.ReactNode;
- callbackUrl: string;
- className?: string;
- icon?: string;
-}) {
- const [integrationMethod, setIntegrationMethod] = useState(
- undefined
- );
-
- const authMethods = Object.values(integration.authenticationMethods);
- const hasApiKeyOption = authMethods.some((s) => s.type === "apikey");
- const hasOAuth2Option = authMethods.some((s) => s.type === "oauth2");
-
- return (
-
- {button}
-
-
-
-
-
{integration.name}
- {integration.description && (
-
{integration.description}
- )}
-
-
- View examples
-
-
- View docs
-
-
-
- Choose an integration method
- setIntegrationMethod(v as IntegrationMethod)}
- >
- {hasOAuth2Option && (
-
- )}
- {hasApiKeyOption && (
-
- )}
-
-
- {integrationMethod && (
-
- )}
-
-
-
- );
-}
-
-function SelectedIntegrationMethod({
- integration,
- organizationId,
- method,
- callbackUrl,
-}: {
- integration: Integration;
- organizationId: string;
- method: IntegrationMethod;
- callbackUrl: string;
-}) {
- const authMethods = Object.values(integration.authenticationMethods);
-
- switch (method) {
- case "apikey":
- const apiAuth = authMethods.find((a) => a.type === "apikey");
- if (!apiAuth) return null;
- return ;
- case "oauth2":
- return (
-
- );
- }
-}
diff --git a/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx b/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx
deleted file mode 100644
index 3f4e6159a8..0000000000
--- a/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-import { conform, useForm } from "@conform-to/react";
-import { parse } from "@conform-to/zod";
-import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
-import type { ConnectionType } from "@trigger.dev/database";
-import cuid from "cuid";
-import { useState } from "react";
-import simplur from "simplur";
-import { useFeatures } from "~/hooks/useFeatures";
-import { useTextFilter } from "~/hooks/useTextFilter";
-import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
-import { ApiAuthenticationMethodOAuth2, Integration, Scope } from "~/services/externalApis/types";
-import { cn } from "~/utils/cn";
-import { CodeBlock } from "../code/CodeBlock";
-import { Button } from "../primitives/Buttons";
-import { CheckboxWithLabel } from "../primitives/Checkbox";
-import { Fieldset } from "../primitives/Fieldset";
-import { FormError } from "../primitives/FormError";
-import { Header2, Header3 } from "../primitives/Headers";
-import { Hint } from "../primitives/Hint";
-import { Input } from "../primitives/Input";
-import { InputGroup } from "../primitives/InputGroup";
-import { Label } from "../primitives/Label";
-import { Paragraph } from "../primitives/Paragraph";
-
-export type Status = "loading" | "idle";
-
-export function ConnectToOAuthForm({
- integration,
- authMethod,
- authMethodKey,
- organizationId,
- clientType,
- callbackUrl,
-}: {
- integration: Integration;
- authMethod: ApiAuthenticationMethodOAuth2;
- authMethodKey: string;
- organizationId: string;
- clientType: ConnectionType;
- callbackUrl: string;
-}) {
- const [id] = useState(cuid());
- const transition = useNavigation();
- const fetcher = useFetcher();
- const { isManagedCloud } = useFeatures();
-
- const [form, { title, slug, scopes, hasCustomClient, customClientId, customClientSecret }] =
- useForm({
- // TODO: type this
- lastSubmission: fetcher.data as any,
- shouldRevalidate: "onSubmit",
- onValidate({ formData }) {
- return parse(formData, {
- // Create the schema without any constraint defined
- schema: createSchema(),
- });
- },
- });
-
- const location = useLocation();
-
- const [selectedScopes, setSelectedScopes] = useState>(
- new Set(authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name))
- );
-
- const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
-
- const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp);
-
- const { filterText, setFilterText, filteredItems } = useTextFilter({
- items: authMethod.scopes,
- filter: (scope, text) => {
- if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
- if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase()))
- return true;
-
- return false;
- },
- });
-
- return (
-
-
-
-
-
-
-
- {form.error}
-
-
- ID
-
-
- This is used in your code to reference this connection. It must be unique for this
- project.
-
- {slug.error}
-
-
- Name
-
- {title.error}
-
-
-
-
Use my OAuth App
-
- To use your own OAuth app, check the option below and insert the details.
-
-
setUseMyOAuthApp(checked)}
- {...conform.input(hasCustomClient, { type: "checkbox" })}
- defaultChecked={requiresCustomOAuthApp}
- />
- {useMyOAuthApp && (
-
-
- Set the callback url to
-
-
-
-
- )}
-
- {authMethod.scopes.length > 0 && (
-
-
Scopes
-
- Select the scopes you want to grant to {integration.name} in order for it to access
- your data. Note: If you try and perform an action in a Job that requires a scope you
- haven’t granted, that task will fail.
-
- {/*
- Select from popular scope collections
-
-
-
- */}
-
-
Select {integration.name} scopes
-
- {simplur`${selectedScopes.size} scope[|s] selected`}
-
-
-
setFilterText(e.target.value)}
- />
-
- {filteredItems.length === 0 && (
-
- No scopes match {filterText}. Try a different search query.
-
- )}
- {authMethod.scopes.map((s) => {
- return (
-
a.label)}
- description={s.description}
- variant="description"
- className={cn(filteredItems.find((f) => f.name === s.name) ? "" : "hidden")}
- onChange={(isChecked) => {
- if (isChecked) {
- setSelectedScopes((selected) => {
- selected.add(s.name);
- return new Set(selected);
- });
- } else {
- setSelectedScopes((selected) => {
- selected.delete(s.name);
- return new Set(selected);
- });
- }
- }}
- />
- );
- })}
-
-
- )}
-
-
-
- {scopes.error}
-
- {`Connect to ${integration.name}`}
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/integrations/CustomHelp.tsx b/apps/webapp/app/components/integrations/CustomHelp.tsx
deleted file mode 100644
index d47602371a..0000000000
--- a/apps/webapp/app/components/integrations/CustomHelp.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { useState } from "react";
-import { CodeExample } from "~/routes/resources.codeexample";
-import { Api } from "~/services/externalApis/apis.server";
-import { cn } from "~/utils/cn";
-import { Feedback } from "../Feedback";
-import { Header1, Header2, Header3 } from "../primitives/Headers";
-import { Paragraph } from "../primitives/Paragraph";
-import { TextLink } from "../primitives/TextLink";
-
-export function CustomHelp({ api }: { api: Api }) {
- const [selectedExample, setSelectedExample] = useState(0);
-
- const changeCodeExample = (index: number) => {
- setSelectedExample(index);
- };
-
- return (
-
-
Using {api.name} with an SDK or requests
-
- You can use Trigger.dev with any existing Node SDK or even just using fetch. You can
- subscribe to any API with{" "}
-
- HTTP endpoints
- {" "}
- and perform actions by wrapping tasks using{" "}
-
- io.runTask
-
- . This makes your background job resumable and appear in our dashboard.
-
-
- {api.examples && api.examples.length > 0 ? (
- <>
-
Example {api.name} code
-
- This is how you can use {api.name} with Trigger.dev. This code can be copied and
- modified to suit your use-case.
-
- {api.examples.length > 1 && (
-
- {api.examples?.map((example, index) => (
- changeCodeExample(index)}
- key={example.codeUrl}
- className={cn(
- "w-64 min-w-[16rem] p-2 transition-colors duration-300 sm:w-full sm:rounded",
- "border-px focus:border-px cursor-pointer border border-charcoal-900 bg-charcoal-900 text-charcoal-300 transition duration-300 hover:bg-charcoal-800 focus:border focus:border-indigo-600"
- )}
- >
- {example.title}
-
- ))}
-
- )}
-
- >
- ) : (
- <>
-
Getting started with {api.name}
-
- We recommend searching for the official {api.name} Node SDK. If they have one, you can
- install it and then use their API documentation to get started and create tasks. If they
- don't, there are often third party SDKs you can use instead.
-
-
- Please{" "}
-
- reach out to us
-
- }
- defaultValue="help"
- />{" "}
- if you're having any issues connecting to {api.name}, we'll help you get set up as
- quickly as possible.
-
- >
- )}
-
- );
-}
diff --git a/apps/webapp/app/components/integrations/HelpInstall.tsx b/apps/webapp/app/components/integrations/HelpInstall.tsx
deleted file mode 100644
index 468e98d108..0000000000
--- a/apps/webapp/app/components/integrations/HelpInstall.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Integration } from "~/services/externalApis/types";
-import { InlineCode } from "../code/InlineCode";
-import {
- ClientTabs,
- ClientTabsList,
- ClientTabsTrigger,
- ClientTabsContent,
-} from "../primitives/ClientTabs";
-import { ClipboardField } from "../primitives/ClipboardField";
-import { Paragraph } from "../primitives/Paragraph";
-
-export function HelpInstall({ packageName }: { packageName: string }) {
- return (
- <>
-
-
- npm
- pnpm
- yarn
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/webapp/app/components/integrations/HelpSamples.tsx b/apps/webapp/app/components/integrations/HelpSamples.tsx
deleted file mode 100644
index 52a8d673c7..0000000000
--- a/apps/webapp/app/components/integrations/HelpSamples.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Integration } from "~/services/externalApis/types";
-import { HelpPanelIntegration, HelpPanelProps } from "./ApiKeyHelp";
-import { Paragraph } from "../primitives/Paragraph";
-import { CodeBlock } from "../code/CodeBlock";
-
-export type ReplacementData = {
- slug: string;
-};
-
-export function HelpSamples({ help, integrationClient, integration }: HelpPanelProps) {
- return (
- <>
- {help &&
- help.samples.map((sample, i) => {
- const code = runReplacers(sample.code, integrationClient, integration);
- return (
-
- );
- })}
- >
- );
-}
-
-const replacements = [
- {
- match: /__SLUG__/g,
- replacement: (data: ReplacementData | undefined, integration: HelpPanelIntegration) => {
- if (data) return data.slug;
- return integration.identifier;
- },
- },
-];
-
-function runReplacers(
- code: string,
- replacementData: ReplacementData | undefined,
- integration: HelpPanelIntegration
-) {
- replacements.forEach((r) => {
- code = code.replace(r.match, r.replacement(replacementData, integration));
- });
-
- return code;
-}
diff --git a/apps/webapp/app/components/integrations/IntegrationWithMissingFieldSheet.tsx b/apps/webapp/app/components/integrations/IntegrationWithMissingFieldSheet.tsx
deleted file mode 100644
index 44b1051041..0000000000
--- a/apps/webapp/app/components/integrations/IntegrationWithMissingFieldSheet.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { docsIntegrationPath } from "~/utils/pathBuilder";
-import { LinkButton } from "../primitives/Buttons";
-import { Header1 } from "../primitives/Headers";
-import { NamedIconInBox } from "../primitives/NamedIcon";
-import { Paragraph } from "../primitives/Paragraph";
-import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
-import { SelectOAuthMethod } from "./SelectOAuthMethod";
-import { Integration } from "~/services/externalApis/types";
-import { Client } from "~/presenters/IntegrationsPresenter.server";
-
-export function IntegrationWithMissingFieldSheet({
- integration,
- organizationId,
- button,
- callbackUrl,
- existingIntegration,
- className,
-}: {
- integration: Integration;
- organizationId: string;
- button: React.ReactNode;
- callbackUrl: string;
- existingIntegration: Client;
- className?: string;
-}) {
- return (
-
- {button}
-
-
-
-
-
{integration.name}
- {integration.description && (
-
{integration.description}
- )}
-
-
- View docs
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/integrations/NoIntegrationSheet.tsx b/apps/webapp/app/components/integrations/NoIntegrationSheet.tsx
deleted file mode 100644
index 6998ffbaba..0000000000
--- a/apps/webapp/app/components/integrations/NoIntegrationSheet.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useFetcher } from "@remix-run/react";
-import React from "react";
-import { Api } from "~/services/externalApis/apis.server";
-import { Header1 } from "../primitives/Headers";
-import { NamedIconInBox } from "../primitives/NamedIcon";
-import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
-import { CustomHelp } from "./CustomHelp";
-
-export function NoIntegrationSheet({
- api,
- requested,
- button,
-}: {
- api: Api;
- requested: boolean;
- button: React.ReactNode;
-}) {
- const fetcher = useFetcher();
- const isLoading = fetcher.state !== "idle";
-
- return (
-
- {button}
-
-
-
-
- {api.name}
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/integrations/SelectOAuthMethod.tsx b/apps/webapp/app/components/integrations/SelectOAuthMethod.tsx
deleted file mode 100644
index 127031e51c..0000000000
--- a/apps/webapp/app/components/integrations/SelectOAuthMethod.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { useState } from "react";
-import { ApiAuthenticationMethodOAuth2, Integration } from "~/services/externalApis/types";
-import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
-import type { ConnectionType } from "@trigger.dev/database";
-import { Header2 } from "../primitives/Headers";
-import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
-import { Paragraph } from "../primitives/Paragraph";
-import { Client } from "~/presenters/IntegrationsPresenter.server";
-import { UpdateOAuthForm } from "./UpdateOAuthForm";
-import { LinkButton } from "../primitives/Buttons";
-import { BookOpenIcon } from "@heroicons/react/20/solid";
-
-export function SelectOAuthMethod({
- integration,
- organizationId,
- callbackUrl,
- existingIntegration,
-}: {
- existingIntegration?: Client;
- integration: Integration;
- organizationId: string;
- callbackUrl: string;
-}) {
- const oAuthMethods = Object.entries(integration.authenticationMethods).filter(
- (a): a is [string, ApiAuthenticationMethodOAuth2] => a[1].type === "oauth2"
- );
-
- const [oAuthKey, setOAuthKey] = useState(
- oAuthMethods.length === 1 ? oAuthMethods[0][0] : undefined
- );
- const [connectionType, setConnectionType] = useState();
-
- const selectedOAuthMethod = oAuthKey
- ? (integration.authenticationMethods[oAuthKey] as ApiAuthenticationMethodOAuth2)
- : undefined;
-
- return (
- <>
- {oAuthMethods.length > 1 && (
- <>
- Select an OAuth option
- setOAuthKey(v)}
- >
- {oAuthMethods.map(([key, auth]) => (
-
- ))}
-
- >
- )}
- {selectedOAuthMethod && (
- <>
- Who is connecting to {integration.name}
- setConnectionType(v as ConnectionType)}
- >
-
-
-
- >
- )}
- {selectedOAuthMethod &&
- connectionType &&
- oAuthKey &&
- (connectionType === "DEVELOPER" ? (
- existingIntegration ? (
-
- ) : (
-
- )
- ) : (
- <>
- BYO Auth
-
- We support external authentication providers through Auth Resolvers. Read the docs to
- learn more:{" "}
-
- Bring your own Auth
-
-
- >
- ))}
- >
- );
-}
diff --git a/apps/webapp/app/components/integrations/UpdateOAuthForm.tsx b/apps/webapp/app/components/integrations/UpdateOAuthForm.tsx
deleted file mode 100644
index f4e0f1fe2d..0000000000
--- a/apps/webapp/app/components/integrations/UpdateOAuthForm.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import { conform, useForm } from "@conform-to/react";
-import { parse } from "@conform-to/zod";
-import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
-import type { ConnectionType } from "@trigger.dev/database";
-import { useState } from "react";
-import simplur from "simplur";
-import { useFeatures } from "~/hooks/useFeatures";
-import { useTextFilter } from "~/hooks/useTextFilter";
-import { ApiAuthenticationMethodOAuth2, Integration, Scope } from "~/services/externalApis/types";
-import { cn } from "~/utils/cn";
-import { CodeBlock } from "../code/CodeBlock";
-import { Button } from "../primitives/Buttons";
-import { CheckboxWithLabel } from "../primitives/Checkbox";
-import { Fieldset } from "../primitives/Fieldset";
-import { FormError } from "../primitives/FormError";
-import { Header2, Header3 } from "../primitives/Headers";
-import { Input } from "../primitives/Input";
-import { InputGroup } from "../primitives/InputGroup";
-import { Label } from "../primitives/Label";
-import { Paragraph } from "../primitives/Paragraph";
-import { Client } from "~/presenters/IntegrationsPresenter.server";
-import { schema } from "~/routes/resources.connection.$organizationId.oauth2.$integrationId";
-
-export type Status = "loading" | "idle";
-
-export function UpdateOAuthForm({
- existingIntegration,
- integration,
- authMethod,
- authMethodKey,
- organizationId,
- clientType,
- callbackUrl,
-}: {
- existingIntegration: Client;
- integration: Integration;
- authMethod: ApiAuthenticationMethodOAuth2;
- authMethodKey: string;
- organizationId: string;
- clientType: ConnectionType;
- callbackUrl: string;
-}) {
- const transition = useNavigation();
- const fetcher = useFetcher();
- const { isManagedCloud } = useFeatures();
-
- const [form, { title, scopes, hasCustomClient, customClientId, customClientSecret }] = useForm({
- // TODO: type this
- lastSubmission: fetcher.data as any,
- onValidate({ formData }) {
- return parse(formData, {
- schema,
- });
- },
- });
-
- const location = useLocation();
-
- const [selectedScopes, setSelectedScopes] = useState>(
- new Set(authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name))
- );
-
- const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
-
- const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp);
-
- const { filterText, setFilterText, filteredItems } = useTextFilter({
- items: authMethod.scopes,
- filter: (scope, text) => {
- if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
- if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase()))
- return true;
-
- return false;
- },
- });
-
- return (
-
-
-
-
-
-
-
- {form.error}
-
-
- ID
-
- {existingIntegration.slug}
-
-
-
- Name
-
- {title.error}
-
-
-
-
Use my OAuth App
-
- To use your own OAuth app, check the option below and insert the details.
-
-
setUseMyOAuthApp(checked)}
- {...conform.input(hasCustomClient, { type: "checkbox" })}
- defaultChecked={requiresCustomOAuthApp}
- />
- {useMyOAuthApp && (
-
-
- Set the callback url to
-
-
-
- )}
-
- {authMethod.scopes.length > 0 && (
-
-
Scopes
-
- Select the scopes you want to grant to {integration.name} in order for it to access
- your data. Note: If you try and perform an action in a Job that requires a scope you
- haven’t granted, that task will fail.
-
- {/*
- Select from popular scope collections
-
-
-
- */}
-
-
Select {integration.name} scopes
-
- {simplur`${selectedScopes.size} scope[|s] selected`}
-
-
-
setFilterText(e.target.value)}
- />
-
- {filteredItems.length === 0 && (
-
- No scopes match {filterText}. Try a different search query.
-
- )}
- {authMethod.scopes.map((s) => {
- return (
-
a.label)}
- description={s.description}
- variant="description"
- className={cn(filteredItems.find((f) => f.name === s.name) ? "" : "hidden")}
- onChange={(isChecked) => {
- if (isChecked) {
- setSelectedScopes((selected) => {
- selected.add(s.name);
- return new Set(selected);
- });
- } else {
- setSelectedScopes((selected) => {
- selected.delete(s.name);
- return new Set(selected);
- });
- }
- }}
- />
- );
- })}
-
-
- )}
-
-
-
- {scopes.error}
-
- {`Connect to ${integration.name}`}
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/integrations/connectionType.ts b/apps/webapp/app/components/integrations/connectionType.ts
deleted file mode 100644
index bd2359c7c2..0000000000
--- a/apps/webapp/app/components/integrations/connectionType.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { ConnectionType } from "@trigger.dev/database";
-
-export function connectionType(type: ConnectionType) {
- switch (type) {
- case "DEVELOPER":
- return "Developer";
- case "EXTERNAL":
- return "Your users";
- }
-}
diff --git a/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx b/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx
deleted file mode 100644
index c53a85088d..0000000000
--- a/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { useFetcher } from "@remix-run/react";
-import { useEffect } from "react";
-import { loader } from "~/routes/resources.jobs.$jobId";
-import { cn } from "~/utils/cn";
-import { JobEnvironment, JobStatusTable } from "../JobsStatusTable";
-import { Button } from "../primitives/Buttons";
-import { Header1, Header2 } from "../primitives/Headers";
-import { NamedIcon } from "../primitives/NamedIcon";
-import { Paragraph } from "../primitives/Paragraph";
-import { Spinner } from "../primitives/Spinner";
-import { TextLink } from "../primitives/TextLink";
-import { useTypedFetcher } from "remix-typedjson";
-
-export function DeleteJobDialog({ id, title, slug }: { id: string; title: string; slug: string }) {
- const fetcher = useTypedFetcher();
- useEffect(() => {
- fetcher.load(`/resources/jobs/${id}`);
- }, [id]);
-
- const isLoading = fetcher.state === "loading" || fetcher.state === "submitting";
-
- if (isLoading || !fetcher.data) {
- return (
-
- );
- } else {
- return (
-
- );
- }
-}
-
-type DeleteJobDialogContentProps = {
- id: string;
- title: string;
- slug: string;
- environments: JobEnvironment[];
- redirectTo?: string;
-};
-
-export function DeleteJobDialogContent({
- title,
- slug,
- environments,
- id,
- redirectTo,
-}: DeleteJobDialogContentProps) {
- const canDelete = environments.every((environment) => !environment.enabled);
- const fetcher = useFetcher();
-
- const isLoading =
- fetcher.state === "submitting" ||
- (fetcher.state === "loading" && fetcher.formMethod === "DELETE");
-
- return (
-
-
-
-
-
- {canDelete
- ? "Are you sure you want to delete this Job?"
- : "You can't delete this Job until all env are disabled"}
-
-
- {canDelete ? (
- <>
- This will permanently delete the Job{" "}
- {title} . This includes the deletion of
- all Run history. This cannot be undone.
- >
- ) : (
- <>
- This Job is still active in an environment. You need to disable it in your Job code
- first before it can be deleted.{" "}
-
- Learn how to disable a Job
-
- .
- >
- )}
-
- {canDelete ? (
-
-
- {isLoading ? (
-
- ) : (
- <>
-
- Delete this Job
- >
- )}
-
-
- ) : (
-
- <>
-
- Delete this Job
- >
-
- )}
-
- );
-}
diff --git a/apps/webapp/app/components/jobs/JobSkeleton.tsx b/apps/webapp/app/components/jobs/JobSkeleton.tsx
deleted file mode 100644
index 3b9d48d030..0000000000
--- a/apps/webapp/app/components/jobs/JobSkeleton.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-export function JobSkeleton() {
- return (
-
- );
-}
diff --git a/apps/webapp/app/components/jobs/JobStatusBadge.tsx b/apps/webapp/app/components/jobs/JobStatusBadge.tsx
deleted file mode 100644
index a9de0c1464..0000000000
--- a/apps/webapp/app/components/jobs/JobStatusBadge.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { ActiveBadge, MissingIntegrationBadge, NewBadge } from "../ActiveBadge";
-
-type JobStatusBadgeProps = {
- enabled: boolean;
- hasIntegrationsRequiringAction: boolean;
- hasRuns: boolean;
- badgeSize?: "small" | "normal";
-};
-
-export function JobStatusBadge({
- enabled,
- hasIntegrationsRequiringAction,
- hasRuns,
- badgeSize = "normal",
-}: JobStatusBadgeProps) {
- if (!enabled) {
- return ;
- }
-
- if (hasIntegrationsRequiringAction) {
- return ;
- }
-
- if (!hasRuns) {
- return ;
- }
-
- return ;
-}
diff --git a/apps/webapp/app/components/jobs/JobsTable.tsx b/apps/webapp/app/components/jobs/JobsTable.tsx
deleted file mode 100644
index 451fa9b868..0000000000
--- a/apps/webapp/app/components/jobs/JobsTable.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-import { useOrganization } from "~/hooks/useOrganizations";
-import { useProject } from "~/hooks/useProject";
-import { JobRunStatus } from "~/models/job.server";
-import { ProjectJob } from "~/presenters/JobListPresenter.server";
-import { jobPath, jobTestPath } from "~/utils/pathBuilder";
-import { Button } from "../primitives/Buttons";
-import { DateTime } from "../primitives/DateTime";
-import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
-import { LabelValueStack } from "../primitives/LabelValueStack";
-import { NamedIcon } from "../primitives/NamedIcon";
-import { Paragraph } from "../primitives/Paragraph";
-import { PopoverMenuItem } from "../primitives/Popover";
-import {
- Table,
- TableBlankRow,
- TableBody,
- TableCell,
- TableCellMenu,
- TableHeader,
- TableHeaderCell,
- TableRow,
-} from "../primitives/Table";
-import { SimpleTooltip } from "../primitives/Tooltip";
-import { runStatusTitle } from "../runs/RunStatuses";
-import { DeleteJobDialog, DeleteJobDialogContent } from "./DeleteJobModalContent";
-import { JobStatusBadge } from "./JobStatusBadge";
-
-export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
- const organization = useOrganization();
-
- return (
-
-
-
- Job
- ID
- Integrations
- Properties
- Last run
- Status
- Go to page
-
-
-
- {jobs.length > 0 ? (
- jobs.map((job) => {
- const path = jobPath(organization, { slug: job.projectSlug }, job);
- return (
-
-
-
-
-
- {" "}
- Dynamic: {job.event.title}
-
- ) : (
- job.event.title
- )
- }
- variant="primary"
- />
-
-
-
-
-
-
- {job.integrations.map((integration) => (
-
-
- {integration.setupStatus === "MISSING_FIELDS" && (
-
- )}
-
- }
- content={
-
-
- {integration.setupStatus === "MISSING_FIELDS" &&
- "This integration requires configuration"}
-
-
- {integration.title}: {integration.key}
-
-
- }
- />
- ))}
-
-
- {job.properties && (
-
- {job.properties.map((property, index) => (
-
- ))}
-
- }
- content={
-
- {job.properties.map((property, index) => (
-
- ))}
-
- }
- />
- )}
-
-
- {job.lastRun ? (
-
- {runStatusTitle(job.lastRun.status)}
-
- }
- value={
-
-
-
- }
- />
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- Delete Job
-
-
-
- Delete Job
-
-
-
-
-
- );
- })
- ) : (
-
-
- {noResultsText}
-
-
- )}
-
-
- );
-}
-
-function classForJobStatus(status: JobRunStatus) {
- switch (status) {
- case "FAILURE":
- case "TIMED_OUT":
- case "WAITING_ON_CONNECTIONS":
- case "PENDING":
- case "UNRESOLVED_AUTH":
- case "INVALID_PAYLOAD":
- return "text-rose-500";
- default:
- return "";
- }
-}
diff --git a/apps/webapp/app/components/layout/app-container-gradient.svg b/apps/webapp/app/components/layout/app-container-gradient.svg
deleted file mode 100644
index f9f710d393..0000000000
--- a/apps/webapp/app/components/layout/app-container-gradient.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/webapp/app/components/run/RunCard.tsx b/apps/webapp/app/components/run/RunCard.tsx
deleted file mode 100644
index 2dfcbf5f63..0000000000
--- a/apps/webapp/app/components/run/RunCard.tsx
+++ /dev/null
@@ -1,265 +0,0 @@
-import type { DisplayProperty, StyleName } from "@trigger.dev/core";
-import { formatDuration } from "@trigger.dev/core/v3";
-import { motion } from "framer-motion";
-import { HourglassIcon } from "lucide-react";
-import { ReactNode, useEffect, useState } from "react";
-import { CodeBlock } from "~/components/code/CodeBlock";
-import { Callout } from "~/components/primitives/Callout";
-import { LabelValueStack } from "~/components/primitives/LabelValueStack";
-import { NamedIcon } from "~/components/primitives/NamedIcon";
-import { Paragraph } from "~/components/primitives/Paragraph";
-import { cn } from "~/utils/cn";
-
-type RunPanelProps = {
- selected?: boolean;
- children: React.ReactNode;
- onClick?: () => void;
- className?: string;
- styleName?: StyleName;
-};
-
-export function RunPanel({
- selected = false,
- children,
- onClick,
- className,
- styleName = "normal",
-}: RunPanelProps) {
- return (
- onClick && onClick()}
- >
- {children}
-
- );
-}
-
-type RunPanelHeaderProps = {
- icon: React.ReactNode;
- title: React.ReactNode;
- accessory?: React.ReactNode;
- styleName?: StyleName;
-};
-
-export function RunPanelHeader({
- icon,
- title,
- accessory,
- styleName = "normal",
-}: RunPanelHeaderProps) {
- return (
-
-
- {typeof icon === "string" ?
: icon}
- {typeof title === "string" ?
{title} : title}
-
-
{accessory}
-
- );
-}
-
-type RunPanelIconTitleProps = {
- icon?: string | null;
- title: string;
-};
-
-export function RunPanelIconTitle({ icon, title }: RunPanelIconTitleProps) {
- return (
-
- );
-}
-
-export function RunPanelBody({ children }: { children: React.ReactNode }) {
- return {children}
;
-}
-
-const variantClasses: Record = {
- log: "",
- error: "text-rose-500",
- warn: "text-yellow-500",
- info: "",
- debug: "",
-};
-
-export function RunPanelDescription({ text, variant }: { text: string; variant?: string }) {
- return (
-
- {text}
-
- );
-}
-
-export function RunPanelError({
- text,
- error,
- stackTrace,
-}: {
- text: string;
- error?: string;
- stackTrace?: string;
-}) {
- return (
-
-
- {text}
-
- {error && }
- {stackTrace && }
-
- );
-}
-
-export function RunPanelIconSection({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return {children}
;
-}
-
-export function RunPanelDivider() {
- return
;
-}
-
-export function RunPanelIconProperty({
- icon,
- label,
- value,
-}: {
- icon: ReactNode;
- label: string;
- value: ReactNode;
-}) {
- return (
-
-
- {typeof icon === "string" ? : icon}
-
-
-
- );
-}
-
-export function RunPanelProperties({
- properties,
- className,
- layout = "horizontal",
-}: {
- properties: DisplayProperty[];
- className?: string;
- layout?: "horizontal" | "vertical";
-}) {
- return (
-
- {properties.map(({ label, text, url }, index) => (
-
- ))}
-
- );
-}
-
-export function TaskSeparator({ depth }: { depth: number }) {
- return (
-
- );
-}
-
-const updateInterval = 100;
-
-export function UpdatingDuration({ start, end }: { start?: Date; end?: Date }) {
- const [now, setNow] = useState();
-
- useEffect(() => {
- if (end) return;
-
- const interval = setInterval(() => {
- setNow(new Date());
- }, updateInterval);
-
- return () => clearInterval(interval);
- }, [end]);
-
- return (
-
- {formatDuration(start, end || now, {
- style: "short",
- maxDecimalPoints: 0,
- })}
-
- );
-}
-
-export function UpdatingDelay({ delayUntil }: { delayUntil: Date }) {
- const [now, setNow] = useState();
-
- useEffect(() => {
- const interval = setInterval(() => {
- const date = new Date();
- if (date > delayUntil) {
- setNow(delayUntil);
- return;
- }
- setNow(date);
- }, updateInterval);
-
- return () => clearInterval(interval);
- }, [delayUntil]);
-
- return (
-
-
-
- }
- label="Delay finishes in"
- value={formatDuration(now, delayUntil, {
- style: "long",
- maxDecimalPoints: 0,
- })}
- />
- );
-}
diff --git a/apps/webapp/app/components/run/RunCompletedDetail.tsx b/apps/webapp/app/components/run/RunCompletedDetail.tsx
deleted file mode 100644
index f11a9bd563..0000000000
--- a/apps/webapp/app/components/run/RunCompletedDetail.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { CodeBlock } from "~/components/code/CodeBlock";
-import { DateTime } from "~/components/primitives/DateTime";
-import { Paragraph } from "~/components/primitives/Paragraph";
-import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
-import { MatchedRun } from "~/hooks/useRun";
-import {
- RunPanel,
- RunPanelBody,
- RunPanelDivider,
- RunPanelError,
- RunPanelHeader,
- RunPanelIconProperty,
- RunPanelIconSection,
-} from "./RunCard";
-import { formatDuration } from "@trigger.dev/core/v3";
-
-export function RunCompletedDetail({ run }: { run: MatchedRun }) {
- return (
-
- }
- title={
-
-
-
- }
- />
-
-
- {run.startedAt && (
- }
- />
- )}
- {run.completedAt && (
- }
- />
- )}
- {run.startedAt && run.completedAt && (
-
- )}
-
-
- {run.error && }
- {run.output ? (
-
- ) : (
- run.output === null && This run returned nothing
- )}
-
-
- );
-}
diff --git a/apps/webapp/app/components/run/RunOverview.tsx b/apps/webapp/app/components/run/RunOverview.tsx
deleted file mode 100644
index cdaf64f44f..0000000000
--- a/apps/webapp/app/components/run/RunOverview.tsx
+++ /dev/null
@@ -1,435 +0,0 @@
-import { conform, useForm } from "@conform-to/react";
-import { parse } from "@conform-to/zod";
-import { PlayIcon } from "@heroicons/react/20/solid";
-import { BoltIcon } from "@heroicons/react/24/solid";
-import {
- Form,
- Outlet,
- useActionData,
- useLocation,
- useNavigate,
- useNavigation,
-} from "@remix-run/react";
-import { RuntimeEnvironmentType, User } from "@trigger.dev/database";
-import { useMemo } from "react";
-import { usePathName } from "~/hooks/usePathName";
-import type { RunBasicStatus } from "~/models/jobRun.server";
-import { ViewRun } from "~/presenters/RunPresenter.server";
-import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
-import { schema } from "~/routes/resources.runs.$runId.rerun";
-import { cn } from "~/utils/cn";
-import { runCompletedPath, runTaskPath, runTriggerPath } from "~/utils/pathBuilder";
-import { CodeBlock } from "../code/CodeBlock";
-import { EnvironmentLabel } from "../environments/EnvironmentLabel";
-import { PageBody, PageContainer } from "../layout/AppLayout";
-import { Button } from "../primitives/Buttons";
-import { Callout } from "../primitives/Callout";
-import { DateTime } from "../primitives/DateTime";
-import { Header2 } from "../primitives/Headers";
-import { Icon } from "../primitives/Icon";
-import { NamedIcon } from "../primitives/NamedIcon";
-import {
- PageAccessories,
- NavBar,
- PageInfoGroup,
- PageInfoProperty,
- PageInfoRow,
- PageTitle,
-} from "../primitives/PageHeader";
-import { Paragraph } from "../primitives/Paragraph";
-import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
-import { RunStatusIcon, RunStatusLabel, runStatusTitle } from "../runs/RunStatuses";
-import {
- RunPanel,
- RunPanelBody,
- RunPanelDivider,
- RunPanelError,
- RunPanelHeader,
- RunPanelIconProperty,
- RunPanelIconSection,
- RunPanelProperties,
-} from "./RunCard";
-import { TaskCard } from "./TaskCard";
-import { TaskCardSkeleton } from "./TaskCardSkeleton";
-import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
-
-type RunOverviewProps = {
- run: ViewRun;
- trigger: {
- icon: string;
- title: string;
- };
- showRerun: boolean;
- paths: {
- back: string;
- run: string;
- runsPath: string;
- };
- currentUser: User;
-};
-
-const taskPattern = /\/tasks\/(.*)/;
-
-export function RunOverview({ run, trigger, showRerun, paths, currentUser }: RunOverviewProps) {
- const navigate = useNavigate();
- const pathName = usePathName();
-
- const selectedId = useMemo(() => {
- if (pathName.endsWith("/completed")) {
- return "completed";
- }
-
- if (pathName.endsWith("/trigger")) {
- return "trigger";
- }
-
- const taskMatch = pathName.match(taskPattern);
- const taskId = taskMatch ? taskMatch[1] : undefined;
- if (taskId) {
- return taskId;
- }
- }, [pathName]);
-
- const usernameForEnv =
- currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
-
- return (
-
-
-
-
- {run.isTest && (
-
-
- Test run
-
- )}
- {showRerun && run.isFinished && (
-
- )}
- {!run.isFinished && }
-
-
-
-
-
-
- }
- label={"Status"}
- value={runStatusTitle(run.status)}
- />
- : "Not started yet"}
- />
-
- }
- />
-
- }
- label={"Execution Time"}
- value={formatDurationMilliseconds(run.executionDuration, { style: "short" })}
- />
- }
- label={"Execution Count"}
- value={<>{run.executionCount}>}
- />
-
-
-
- RUN ID: {run.id}
-
-
-
-
-
-
-
- {run.status === "SUCCESS" &&
- (run.tasks.length === 0 || run.tasks.every((t) => t.noop)) && (
-
- This Run completed but it did not use any Tasks – this can cause unpredictable
- results. Read the docs to view the solution.
-
- )}
- Trigger
- navigate(runTriggerPath(paths.run))}
- >
-
-
-
-
-
-
-
- Tasks
-
- {run.tasks.length > 0 ? (
- run.tasks.map((task, index) => {
- const isLast = index === run.tasks.length - 1;
-
- return (
- {
- navigate(runTaskPath(paths.run, taskId));
- }}
- isLast={isLast}
- depth={0}
- {...task}
- />
- );
- })
- ) : (
-
- )}
-
- {(run.basicStatus === "COMPLETED" || run.basicStatus === "FAILED") && (
-
-
Run Summary
-
navigate(runCompletedPath(paths.run))}
- >
- }
- title={
-
-
-
- }
- />
-
-
- {run.startedAt && (
- }
- />
- )}
- {run.completedAt && (
- }
- />
- )}
- {run.startedAt && run.completedAt && (
-
- )}
-
-
- {run.error && (
-
- )}
- {run.output ? (
-
- ) : (
- run.output === null && (
-
- This Run returned nothing.
-
- )
- )}
-
-
-
- )}
-
-
- {/* Detail view */}
-
- Details
- {selectedId ? : Select a task or trigger }
-
-
-
-
- );
-}
-
-function BlankTasks({ status }: { status: RunBasicStatus }) {
- switch (status) {
- default:
- case "COMPLETED":
- return There were no tasks for this run. ;
- case "FAILED":
- return No tasks were run. ;
- case "WAITING":
- case "PENDING":
- case "RUNNING":
- return (
-
-
- Waiting for tasks…
-
-
-
- );
- }
-}
-
-function RerunPopover({
- runId,
- runPath,
- runsPath,
- environmentType,
- status,
-}: {
- runId: string;
- runPath: string;
- runsPath: string;
- environmentType: RuntimeEnvironmentType;
- status: RunBasicStatus;
-}) {
- const lastSubmission = useActionData();
-
- const [form, { successRedirect, failureRedirect }] = useForm({
- id: "rerun",
- // TODO: type this
- lastSubmission: lastSubmission as any,
- onValidate({ formData }) {
- return parse(formData, { schema });
- },
- });
-
- return (
-
-
-
- Rerun Job
-
-
-
-
-
-
- );
-}
-
-export function CancelRun({ runId }: { runId: string }) {
- const lastSubmission = useActionData();
- const location = useLocation();
- const navigation = useNavigation();
-
- const [form, { redirectUrl }] = useForm({
- id: "cancel-run",
- // TODO: type this
- lastSubmission: lastSubmission as any,
- onValidate({ formData }) {
- return parse(formData, { schema: cancelSchema });
- },
- });
-
- const isLoading = navigation.state === "submitting" && navigation.formData !== undefined;
-
- return (
-
- );
-}
diff --git a/apps/webapp/app/components/run/TaskAttemptStatus.tsx b/apps/webapp/app/components/run/TaskAttemptStatus.tsx
deleted file mode 100644
index df87d16297..0000000000
--- a/apps/webapp/app/components/run/TaskAttemptStatus.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/24/solid";
-import type { TaskAttemptStatus } from "@trigger.dev/database";
-import { Paragraph } from "~/components/primitives/Paragraph";
-import { Spinner } from "~/components/primitives/Spinner";
-import { cn } from "~/utils/cn";
-
-type TaskAttemptStatusProps = {
- status: TaskAttemptStatus;
- className?: string;
-};
-
-export function TaskAttemptStatusLabel({ status }: { status: TaskAttemptStatus }) {
- return (
-
-
-
- {taskAttemptStatusTitle(status)}
-
-
- );
-}
-
-export function TaskAttemptStatusIcon({ status, className }: TaskAttemptStatusProps) {
- switch (status) {
- case "COMPLETED":
- return ;
- case "PENDING":
- return ;
- case "STARTED":
- return ;
- case "ERRORED":
- return ;
- }
-}
-
-function taskAttemptStatusClassNameColor(status: TaskAttemptStatus): string {
- switch (status) {
- case "COMPLETED":
- return "text-green-500";
- case "PENDING":
- return "text-charcoal-400";
- case "STARTED":
- return "text-blue-500";
- case "ERRORED":
- return "text-rose-500";
- }
-}
-
-function taskAttemptStatusTitle(status: TaskAttemptStatus): string {
- switch (status) {
- case "COMPLETED":
- return "Complete";
- case "PENDING":
- return "Scheduled";
- case "STARTED":
- return "Running";
- case "ERRORED":
- return "Error";
- }
-}
diff --git a/apps/webapp/app/components/run/TaskCard.tsx b/apps/webapp/app/components/run/TaskCard.tsx
deleted file mode 100644
index a0ccc9e3ac..0000000000
--- a/apps/webapp/app/components/run/TaskCard.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { ChevronDownIcon, Square2StackIcon } from "@heroicons/react/24/solid";
-import { AnimatePresence, motion } from "framer-motion";
-import { Fragment, useState } from "react";
-import simplur from "simplur";
-import { Paragraph } from "~/components/primitives/Paragraph";
-import { ViewTask } from "~/presenters/RunPresenter.server";
-import { cn } from "~/utils/cn";
-import {
- RunPanel,
- RunPanelBody,
- RunPanelDescription,
- RunPanelError,
- RunPanelHeader,
- RunPanelIconProperty,
- RunPanelIconSection,
- RunPanelIconTitle,
- RunPanelProperties,
- TaskSeparator,
- UpdatingDelay,
- UpdatingDuration,
-} from "./RunCard";
-import { TaskStatusIcon } from "./TaskStatus";
-import { formatDuration } from "@trigger.dev/core/v3";
-
-type TaskCardProps = ViewTask & {
- selectedId?: string;
- selectedTask: (id: string) => void;
- isLast: boolean;
- depth: number;
-};
-
-export function TaskCard({
- selectedId,
- selectedTask,
- isLast,
- depth,
- id,
- style,
- status,
- icon,
- name,
- startedAt,
- completedAt,
- description,
- displayKey,
- connection,
- properties,
- subtasks,
- error,
- delayUntil,
-}: TaskCardProps) {
- const [expanded, setExpanded] = useState(false);
- const isSelected = id === selectedId;
-
- return (
-
-
-
selectedTask(id)} styleName={style?.style}>
-
- )
- }
- title={status === "COMPLETED" ? name : }
- accessory={
-
-
-
- }
- styleName={style?.style}
- />
-
- {error && }
- {description && }
-
- {displayKey && }
- {delayUntil && !completedAt && (
- <>
-
- >
- )}
- {delayUntil && completedAt && (
-
- )}
- {connection && (
-
- )}
-
- {properties.length > 0 && (
-
- )}
-
- {subtasks && subtasks.length > 0 && (
- setExpanded((c) => !c)}
- >
-
-
-
- {simplur`${expanded ? "Hide" : "Show"} ${subtasks.length} subtask[|s]`}
-
-
-
-
-
-
- )}
-
-
- {(!isLast || expanded) && }
-
- {subtasks &&
- subtasks.length > 0 &&
- expanded &&
- subtasks.map((subtask, index) => (
-
-
-
-
-
- ))}
-
- );
-}
diff --git a/apps/webapp/app/components/run/TaskCardSkeleton.tsx b/apps/webapp/app/components/run/TaskCardSkeleton.tsx
deleted file mode 100644
index e3708eb87b..0000000000
--- a/apps/webapp/app/components/run/TaskCardSkeleton.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { RunPanel, RunPanelBody, RunPanelHeader } from "./RunCard";
-
-export function TaskCardSkeleton() {
- return (
-
- }
- />
-
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/run/TaskDetail.tsx b/apps/webapp/app/components/run/TaskDetail.tsx
deleted file mode 100644
index d09df85c0c..0000000000
--- a/apps/webapp/app/components/run/TaskDetail.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import {
- RunPanel,
- RunPanelBody,
- RunPanelDescription,
- RunPanelDivider,
- RunPanelHeader,
- RunPanelIconProperty,
- RunPanelIconSection,
- RunPanelIconTitle,
- RunPanelProperties,
- UpdatingDelay,
- UpdatingDuration,
-} from "./RunCard";
-import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
-import { cn } from "~/utils/cn";
-import { CodeBlock } from "../code/CodeBlock";
-import { DateTime } from "../primitives/DateTime";
-import { Header3 } from "../primitives/Headers";
-import { Paragraph } from "../primitives/Paragraph";
-import {
- TableHeader,
- TableRow,
- TableHeaderCell,
- TableBody,
- TableCell,
- Table,
-} from "../primitives/Table";
-import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
-import { TaskStatusIcon } from "./TaskStatus";
-import { ClientOnly } from "remix-utils/client-only";
-import { Spinner } from "../primitives/Spinner";
-import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route";
-import { formatDuration } from "@trigger.dev/core/v3";
-
-export function TaskDetail({ task }: { task: DetailedTask }) {
- const {
- name,
- description,
- icon,
- status,
- params,
- properties,
- output,
- outputIsUndefined,
- style,
- attempts,
- noop,
- } = task;
-
- const startedAt = task.startedAt ? new Date(task.startedAt) : undefined;
- const completedAt = task.completedAt ? new Date(task.completedAt) : undefined;
- const delayUntil = task.delayUntil ? new Date(task.delayUntil) : undefined;
-
- return (
-
- }
- title={ }
- accessory={
-
-
-
- }
- />
-
-
- {startedAt && (
- }
- />
- )}
- {completedAt && (
- }
- />
- )}
- {delayUntil && !completedAt && (
- <>
- }
- />
-
- >
- )}
- {delayUntil && completedAt && (
-
- )}
-
-
- {description && }
- {properties.length > 0 && (
-
- Properties
-
-
- )}
-
- {attempts.length > 1 && (
-
-
Retries
-
-
-
- Attempt
- Status
- Date
- Error
-
-
-
- {attempts.map((attempt) => (
-
- {attempt.number}
-
-
-
-
-
-
- {attempt.error}
-
- ))}
-
-
-
- )}
-
-
-
Input
- {params ? (
-
- ) : (
-
No input
- )}
-
- {!noop && (
-
-
Output
- {output && !outputIsUndefined ? (
-
}>
- {() =>
}
-
- ) : (
-
No output
- )}
-
- )}
-
-
- );
-}
diff --git a/apps/webapp/app/components/run/TaskStatus.tsx b/apps/webapp/app/components/run/TaskStatus.tsx
deleted file mode 100644
index 38a4703a72..0000000000
--- a/apps/webapp/app/components/run/TaskStatus.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { TaskStatus } from "@trigger.dev/core";
-import {
- CheckCircleIcon,
- CheckIcon,
- ClockIcon,
- NoSymbolIcon,
- XCircleIcon,
-} from "@heroicons/react/24/solid";
-import { Spinner } from "~/components/primitives/Spinner";
-import { cn } from "~/utils/cn";
-
-type TaskStatusIconProps = {
- status: TaskStatus;
- className: string;
- minimal?: boolean;
-};
-
-export function TaskStatusIcon({ status, className, minimal = false }: TaskStatusIconProps) {
- switch (status) {
- case "COMPLETED":
- return minimal ? (
-
- ) : (
-
- );
- case "PENDING":
- return ;
- case "WAITING":
- return ;
- case "RUNNING":
- return ;
- case "ERRORED":
- return ;
- case "CANCELED":
- return ;
- }
-}
-
-function taskStatusClassNameColor(status: TaskStatus): string {
- switch (status) {
- case "COMPLETED":
- return "text-green-500";
- case "PENDING":
- return "text-charcoal-500";
- case "RUNNING":
- return "text-blue-500";
- case "WAITING":
- return "text-blue-500";
- case "ERRORED":
- return "text-rose-500";
- case "CANCELED":
- return "text-charcoal-500";
- }
-}
diff --git a/apps/webapp/app/components/run/TriggerDetail.tsx b/apps/webapp/app/components/run/TriggerDetail.tsx
deleted file mode 100644
index aee57b22ea..0000000000
--- a/apps/webapp/app/components/run/TriggerDetail.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { DetailedEvent } from "~/presenters/TriggerDetailsPresenter.server";
-import { CodeBlock } from "../code/CodeBlock";
-import { DateTime } from "../primitives/DateTime";
-import { Header3 } from "../primitives/Headers";
-import {
- RunPanel,
- RunPanelBody,
- RunPanelDivider,
- RunPanelHeader,
- RunPanelIconProperty,
- RunPanelIconSection,
- RunPanelProperties,
-} from "./RunCard";
-import { DisplayProperty } from "@trigger.dev/core";
-
-export function TriggerDetail({
- trigger,
- event,
- properties,
-}: {
- trigger: DetailedEvent;
- event: {
- title: string;
- icon: string;
- };
- properties: DisplayProperty[];
-}) {
- const { id, name, payload, context, timestamp, deliveredAt } = trigger;
-
- return (
-
-
-
-
- }
- />
- {deliveredAt && (
- }
- />
- )}
-
-
- {trigger.externalAccount && (
-
- )}
-
-
-
- {properties.length > 0 && (
-
- Properties
-
-
- )}
-
Payload
-
-
Context
-
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/runs/RunFilters.tsx b/apps/webapp/app/components/runs/RunFilters.tsx
deleted file mode 100644
index b70d2e9d4e..0000000000
--- a/apps/webapp/app/components/runs/RunFilters.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import {
- CheckCircleIcon,
- ClockIcon,
- ExclamationTriangleIcon,
- NoSymbolIcon,
- PauseCircleIcon,
- TrashIcon,
- XCircleIcon,
- XMarkIcon,
-} from "@heroicons/react/20/solid";
-import { useNavigate } from "@remix-run/react";
-import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
-import { cn } from "~/utils/cn";
-import { EnvironmentLabel } from "../environments/EnvironmentLabel";
-import { Paragraph } from "../primitives/Paragraph";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../primitives/SimpleSelect";
-import { Spinner } from "../primitives/Spinner";
-import {
- FilterableEnvironment,
- FilterableStatus,
- RunListSearchSchema,
- environmentKeys,
- statusKeys,
-} from "./RunStatuses";
-import { TimeFrameFilter } from "./TimeFrameFilter";
-import { Button } from "../primitives/Buttons";
-import { useCallback } from "react";
-import assertNever from "assert-never";
-
-export function RunsFilters() {
- const navigate = useNavigate();
- const location = useOptimisticLocation();
- const searchParams = new URLSearchParams(location.search);
- const { environment, status, from, to } = RunListSearchSchema.parse(
- Object.fromEntries(searchParams.entries())
- );
-
- const handleFilterChange = useCallback((filterType: string, value: string | undefined) => {
- if (value) {
- searchParams.set(filterType, value);
- } else {
- searchParams.delete(filterType);
- }
- searchParams.delete("cursor");
- searchParams.delete("direction");
- navigate(`${location.pathname}?${searchParams.toString()}`);
- }, []);
-
- const handleStatusChange = useCallback((value: FilterableStatus | "ALL") => {
- handleFilterChange("status", value === "ALL" ? undefined : value);
- }, []);
-
- const handleEnvironmentChange = useCallback((value: FilterableEnvironment | "ALL") => {
- handleFilterChange("environment", value === "ALL" ? undefined : value);
- }, []);
-
- const handleTimeFrameChange = useCallback((range: { from?: number; to?: number }) => {
- if (range.from) {
- searchParams.set("from", range.from.toString());
- } else {
- searchParams.delete("from");
- }
-
- if (range.to) {
- searchParams.set("to", range.to.toString());
- } else {
- searchParams.delete("to");
- }
-
- searchParams.delete("cursor");
- searchParams.delete("direction");
- navigate(`${location.pathname}?${searchParams.toString()}`);
- }, []);
-
- const clearFilters = useCallback(() => {
- searchParams.delete("status");
- searchParams.delete("environment");
- searchParams.delete("from");
- searchParams.delete("to");
- navigate(`${location.pathname}?${searchParams.toString()}`);
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
- All environments
-
-
- {environmentKeys.map((env) => (
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
- All statuses
-
-
- {statusKeys.map((status) => (
-
- {
-
-
-
-
- }
-
- ))}
-
-
-
-
-
-
-
clearFilters()} LeadingIcon={TrashIcon} />
-
- );
-}
-
-export function FilterStatusLabel({ status }: { status: FilterableStatus }) {
- return {filterStatusTitle(status)} ;
-}
-
-export function FilterStatusIcon({
- status,
- className,
-}: {
- status: FilterableStatus;
- className: string;
-}) {
- switch (status) {
- case "COMPLETED":
- return ;
- case "WAITING":
- return ;
- case "QUEUED":
- return ;
- case "IN_PROGRESS":
- return ;
- case "TIMEDOUT":
- return (
-
- );
- case "CANCELED":
- return ;
- case "FAILED":
- return ;
- default: {
- assertNever(status);
- }
- }
-}
-
-export function filterStatusTitle(status: FilterableStatus): string {
- switch (status) {
- case "QUEUED":
- return "Queued";
- case "IN_PROGRESS":
- return "In progress";
- case "WAITING":
- return "Waiting";
- case "COMPLETED":
- return "Completed";
- case "FAILED":
- return "Failed";
- case "CANCELED":
- return "Canceled";
- case "TIMEDOUT":
- return "Timed out";
- default: {
- assertNever(status);
- }
- }
-}
-
-export function filterStatusClassNameColor(status: FilterableStatus): string {
- switch (status) {
- case "QUEUED":
- return "text-charcoal-500";
- case "IN_PROGRESS":
- return "text-blue-500";
- case "WAITING":
- return "text-blue-500";
- case "COMPLETED":
- return "text-green-500";
- case "FAILED":
- return "text-rose-500";
- case "CANCELED":
- return "text-charcoal-500";
- case "TIMEDOUT":
- return "text-amber-300";
- default: {
- assertNever(status);
- }
- }
-}
diff --git a/apps/webapp/app/components/runs/RunStatuses.tsx b/apps/webapp/app/components/runs/RunStatuses.tsx
deleted file mode 100644
index 8ef4d39828..0000000000
--- a/apps/webapp/app/components/runs/RunStatuses.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import { NoSymbolIcon } from "@heroicons/react/20/solid";
-import {
- CheckCircleIcon,
- ClockIcon,
- ExclamationTriangleIcon,
- PauseCircleIcon,
- WrenchIcon,
- XCircleIcon,
-} from "@heroicons/react/24/solid";
-import type { JobRunStatus } from "@trigger.dev/database";
-import { cn } from "~/utils/cn";
-import { Spinner } from "../primitives/Spinner";
-import { z } from "zod";
-import assertNever from "assert-never";
-
-export function RunStatus({ status }: { status: JobRunStatus }) {
- return (
-
-
-
-
- );
-}
-
-export function RunStatusLabel({ status }: { status: JobRunStatus }) {
- return {runStatusTitle(status)} ;
-}
-
-export function RunStatusIcon({ status, className }: { status: JobRunStatus; className: string }) {
- switch (status) {
- case "SUCCESS":
- return ;
- case "PENDING":
- case "WAITING_TO_CONTINUE":
- return ;
- case "QUEUED":
- case "WAITING_TO_EXECUTE":
- return ;
- case "PREPROCESSING":
- case "STARTED":
- case "EXECUTING":
- return ;
- case "TIMED_OUT":
- return ;
- case "UNRESOLVED_AUTH":
- case "FAILURE":
- case "ABORTED":
- case "INVALID_PAYLOAD":
- return ;
- case "WAITING_ON_CONNECTIONS":
- return ;
- case "CANCELED":
- return ;
- default: {
- assertNever(status);
- }
- }
-}
-
-export function runStatusTitle(status: JobRunStatus): string {
- switch (status) {
- case "SUCCESS":
- return "Completed";
- case "PENDING":
- return "Not started";
- case "STARTED":
- return "In progress";
- case "QUEUED":
- case "WAITING_TO_EXECUTE":
- return "Queued";
- case "EXECUTING":
- return "Executing";
- case "WAITING_TO_CONTINUE":
- return "Waiting";
- case "FAILURE":
- return "Failed";
- case "TIMED_OUT":
- return "Timed out";
- case "WAITING_ON_CONNECTIONS":
- return "Waiting on connections";
- case "ABORTED":
- return "Aborted";
- case "PREPROCESSING":
- return "Preprocessing";
- case "CANCELED":
- return "Canceled";
- case "UNRESOLVED_AUTH":
- return "Unresolved auth";
- case "INVALID_PAYLOAD":
- return "Invalid payload";
- default: {
- assertNever(status);
- }
- }
-}
-
-export function runStatusClassNameColor(status: JobRunStatus): string {
- switch (status) {
- case "SUCCESS":
- return "text-green-500";
- case "PENDING":
- return "text-charcoal-500";
- case "STARTED":
- case "EXECUTING":
- case "WAITING_TO_CONTINUE":
- case "WAITING_TO_EXECUTE":
- return "text-blue-500";
- case "QUEUED":
- return "text-charcoal-500";
- case "FAILURE":
- case "UNRESOLVED_AUTH":
- case "INVALID_PAYLOAD":
- return "text-rose-500";
- case "TIMED_OUT":
- return "text-amber-300";
- case "WAITING_ON_CONNECTIONS":
- return "text-amber-300";
- case "ABORTED":
- return "text-rose-500";
- case "PREPROCESSING":
- return "text-blue-500";
- case "CANCELED":
- return "text-charcoal-500";
- default: {
- assertNever(status);
- }
- }
-}
-
-export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]);
-export type Direction = z.infer;
-
-export const FilterableStatus = z.union([
- z.literal("QUEUED"),
- z.literal("IN_PROGRESS"),
- z.literal("WAITING"),
- z.literal("COMPLETED"),
- z.literal("FAILED"),
- z.literal("TIMEDOUT"),
- z.literal("CANCELED"),
-]);
-export type FilterableStatus = z.infer;
-
-export const FilterableEnvironment = z.union([
- z.literal("DEVELOPMENT"),
- z.literal("STAGING"),
- z.literal("PRODUCTION"),
-]);
-export type FilterableEnvironment = z.infer;
-export const environmentKeys: FilterableEnvironment[] = ["DEVELOPMENT", "STAGING", "PRODUCTION"];
-
-export const RunListSearchSchema = z.object({
- cursor: z.string().optional(),
- direction: DirectionSchema.optional(),
- status: FilterableStatus.optional(),
- environment: FilterableEnvironment.optional(),
- from: z
- .string()
- .transform((value) => parseInt(value))
- .optional(),
- to: z
- .string()
- .transform((value) => parseInt(value))
- .optional(),
-});
-
-export const filterableStatuses: Record = {
- QUEUED: ["QUEUED", "WAITING_TO_EXECUTE", "PENDING", "WAITING_ON_CONNECTIONS"],
- IN_PROGRESS: ["STARTED", "EXECUTING", "PREPROCESSING"],
- WAITING: ["WAITING_TO_CONTINUE"],
- COMPLETED: ["SUCCESS"],
- FAILED: ["FAILURE", "UNRESOLVED_AUTH", "INVALID_PAYLOAD", "ABORTED"],
- TIMEDOUT: ["TIMED_OUT"],
- CANCELED: ["CANCELED"],
-};
-
-export const statusKeys: FilterableStatus[] = Object.keys(filterableStatuses) as FilterableStatus[];
diff --git a/apps/webapp/app/components/runs/RunsTable.tsx b/apps/webapp/app/components/runs/RunsTable.tsx
deleted file mode 100644
index c194da9532..0000000000
--- a/apps/webapp/app/components/runs/RunsTable.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import { StopIcon } from "@heroicons/react/24/outline";
-import { CheckIcon } from "@heroicons/react/24/solid";
-import { JobRunStatus, RuntimeEnvironmentType, User } from "@trigger.dev/database";
-import { EnvironmentLabel } from "../environments/EnvironmentLabel";
-import { DateTime } from "../primitives/DateTime";
-import { Paragraph } from "../primitives/Paragraph";
-import { Spinner } from "../primitives/Spinner";
-import {
- Table,
- TableBlankRow,
- TableBody,
- TableCell,
- TableCellChevron,
- TableHeader,
- TableHeaderCell,
- TableRow,
-} from "../primitives/Table";
-import { RunStatus } from "./RunStatuses";
-import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
-
-type RunTableItem = {
- id: string;
- number: number | null;
- environment: {
- type: RuntimeEnvironmentType;
- userId?: string;
- userName?: string;
- };
- job: { title: string; slug: string };
- status: JobRunStatus;
- startedAt: Date | null;
- completedAt: Date | null;
- createdAt: Date | null;
- executionDuration: number;
- version: string;
- isTest: boolean;
-};
-
-type RunsTableProps = {
- total: number;
- hasFilters: boolean;
- showJob?: boolean;
- runs: RunTableItem[];
- isLoading?: boolean;
- runsParentPath: string;
- currentUser: User;
-};
-
-export function RunsTable({
- total,
- hasFilters,
- runs,
- isLoading = false,
- showJob = false,
- runsParentPath,
- currentUser,
-}: RunsTableProps) {
- return (
-
-
-
- Run
- {showJob && Job }
- Env
- Status
- Started
- Duration
- Exec Time
- Test
- Version
- Created at
-
- Go to page
-
-
-
-
- {total === 0 && !hasFilters ? (
-
- {!isLoading && }
-
- ) : runs.length === 0 ? (
-
- {!isLoading && }
-
- ) : (
- runs.map((run) => {
- const path = showJob
- ? `${runsParentPath}/jobs/${run.job.slug}/runs/${run.id}/trigger`
- : `${runsParentPath}/${run.id}/trigger`;
- const usernameForEnv =
- currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
- return (
-
-
- {typeof run.number === "number" ? `#${run.number}` : "-"}
-
- {showJob && {run.job.slug} }
-
-
-
-
-
-
-
- {run.startedAt ? : "–"}
-
-
- {formatDuration(run.startedAt, run.completedAt, {
- style: "short",
- })}
-
-
- {formatDurationMilliseconds(run.executionDuration, {
- style: "short",
- })}
-
-
- {run.isTest ? (
-
- ) : (
-
- )}
-
- {run.version}
-
- {run.createdAt ? : "–"}
-
-
-
- );
- })
- )}
- {isLoading && (
-
- Loading…
-
- )}
-
-
- );
-}
-
-function NoRuns({ title }: { title: string }) {
- return (
-
- );
-}
diff --git a/apps/webapp/app/components/runs/TimeFrameFilter.tsx b/apps/webapp/app/components/runs/TimeFrameFilter.tsx
deleted file mode 100644
index 87e6fe0f95..0000000000
--- a/apps/webapp/app/components/runs/TimeFrameFilter.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-import { ChevronDownIcon } from "lucide-react";
-import { useCallback, useState } from "react";
-import { cn } from "~/utils/cn";
-import { Button } from "../primitives/Buttons";
-import { ClientTabs, ClientTabsContent, ClientTabsWithUnderline } from "../primitives/ClientTabs";
-import { DateField } from "../primitives/DateField";
-import { formatDateTime } from "../primitives/DateTime";
-import { Paragraph } from "../primitives/Paragraph";
-import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
-import { Label } from "../primitives/Label";
-
-type RunTimeFrameFilterProps = {
- from?: number;
- to?: number;
- onRangeChanged: (range: { from?: number; to?: number }) => void;
-};
-
-type Mode = "absolute" | "relative";
-
-export function TimeFrameFilter({ from, to, onRangeChanged }: RunTimeFrameFilterProps) {
- const [activeTab, setActiveTab] = useState("absolute");
- const [isOpen, setIsOpen] = useState(false);
- const [relativeTimeSeconds, setRelativeTimeSeconds] = useState();
-
- const fromDate = from ? new Date(from) : undefined;
- const toDate = to ? new Date(to) : undefined;
-
- const relativeTimeFrameChanged = useCallback((value: number) => {
- const to = new Date().getTime();
- const from = to - value;
- onRangeChanged({ from, to });
- setRelativeTimeSeconds(value);
- }, []);
-
- const absoluteTimeFrameChanged = useCallback(({ from, to }: { from?: Date; to?: Date }) => {
- setRelativeTimeSeconds(undefined);
- const fromTime = from?.getTime();
- const toTime = to?.getTime();
- onRangeChanged({ from: fromTime, to: toTime });
- }, []);
-
- return (
- setIsOpen(open)} open={isOpen} modal>
-
-
-
- {title(from, to, relativeTimeSeconds)}
-
-
-
-
-
-
- setActiveTab(v as Mode)}
- className="p-1"
- >
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function title(
- from: number | undefined,
- to: number | undefined,
- relativeTimeSeconds: number | undefined
-): string {
- if (!from && !to) {
- return "All time periods";
- }
-
- if (relativeTimeSeconds !== undefined) {
- return timeFrameValues.find((t) => t.value === relativeTimeSeconds)?.label ?? "Timeframe";
- }
-
- let fromString = from ? formatDateTime(new Date(from), "UTC", ["en-US"], false, true) : undefined;
- let toString = to ? formatDateTime(new Date(to), "UTC", ["en-US"], false, true) : undefined;
- if (from && !to) {
- return `From ${fromString} (UTC)`;
- }
-
- if (!from && to) {
- return `To ${toString} (UTC)`;
- }
-
- return `${fromString} - ${toString} (UTC)`;
-}
-
-function RelativeTimeFrame({
- value,
- onValueChange,
-}: {
- value?: number;
- onValueChange: (value: number) => void;
-}) {
- return (
-
- {timeFrameValues.map((timeframe) => (
- {
- onValueChange(timeframe.value);
- }}
- >
- {timeframe.label}
-
- ))}
-
- );
-}
-
-const timeFrameValues = [
- {
- label: "5 mins",
- value: 5 * 60 * 1000,
- },
- {
- label: "15 mins",
- value: 15 * 60 * 1000,
- },
- {
- label: "30 mins",
- value: 30 * 60 * 1000,
- },
- {
- label: "1 hour",
- value: 60 * 60 * 1000,
- },
- {
- label: "3 hours",
- value: 3 * 60 * 60 * 1000,
- },
- {
- label: "6 hours",
- value: 6 * 60 * 60 * 1000,
- },
- {
- label: "1 day",
- value: 24 * 60 * 60 * 1000,
- },
- {
- label: "3 days",
- value: 3 * 24 * 60 * 60 * 1000,
- },
- {
- label: "7 days",
- value: 7 * 24 * 60 * 60 * 1000,
- },
- {
- label: "10 days",
- value: 10 * 24 * 60 * 60 * 1000,
- },
- {
- label: "14 days",
- value: 14 * 24 * 60 * 60 * 1000,
- },
- {
- label: "30 days",
- value: 30 * 24 * 60 * 60 * 1000,
- },
-];
-
-export type RelativeTimeFrameItem = (typeof timeFrameValues)[number];
-
-export function AbsoluteTimeFrame({
- from,
- to,
- onValueChange,
-}: {
- from?: Date;
- to?: Date;
- onValueChange: (value: { from?: Date; to?: Date }) => void;
-}) {
- return (
-
-
-
- From (UTC)
- {
- onValueChange({ from: value, to: to });
- }}
- granularity="second"
- showNowButton
- showClearButton
- utc
- />
-
-
- To (UTC)
- {
- onValueChange({ from: from, to: value });
- }}
- granularity="second"
- showNowButton
- showClearButton
- utc
- />
-
-
-
- );
-}
diff --git a/apps/webapp/app/components/runs/WebhookDeliveryRunsTable.tsx b/apps/webapp/app/components/runs/WebhookDeliveryRunsTable.tsx
deleted file mode 100644
index 7b97fcd022..0000000000
--- a/apps/webapp/app/components/runs/WebhookDeliveryRunsTable.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import { StopIcon } from "@heroicons/react/24/outline";
-import { CheckIcon } from "@heroicons/react/24/solid";
-import { RuntimeEnvironmentType } from "@trigger.dev/database";
-import { EnvironmentLabel } from "../environments/EnvironmentLabel";
-import { DateTime } from "../primitives/DateTime";
-import { Paragraph } from "../primitives/Paragraph";
-import { Spinner } from "../primitives/Spinner";
-import {
- Table,
- TableBlankRow,
- TableBody,
- TableCell,
- TableHeader,
- TableHeaderCell,
- TableRow,
-} from "../primitives/Table";
-import { RunStatus } from "./RunStatuses";
-import { formatDuration } from "@trigger.dev/core/v3";
-
-type RunTableItem = {
- id: string;
- number: number;
- environment: {
- type: RuntimeEnvironmentType;
- };
- error: string | null;
- createdAt: Date | null;
- deliveredAt: Date | null;
- verified: boolean;
-};
-
-type RunsTableProps = {
- total: number;
- hasFilters: boolean;
- runs: RunTableItem[];
- isLoading?: boolean;
- runsParentPath: string;
-};
-
-export function WebhookDeliveryRunsTable({
- total,
- hasFilters,
- runs,
- isLoading = false,
- runsParentPath,
-}: RunsTableProps) {
- return (
-
-
-
- Run
- Env
- Status
- Last Error
- Started
- Duration
- Verified
- Created at
-
-
-
- {total === 0 && !hasFilters ? (
-
-
-
- ) : runs.length === 0 ? (
-
-
-
- ) : (
- runs.map((run) => {
- return (
-
- #{run.number}
-
-
-
-
-
-
- {run.error?.slice(0, 30) ?? "–"}
- {run.createdAt ? : "–"}
-
- {formatDuration(run.createdAt, run.deliveredAt, {
- style: "short",
- })}
-
-
- {run.verified ? (
-
- ) : (
-
- )}
-
- {run.createdAt ? : "–"}
-
- );
- })
- )}
- {isLoading && (
-
- Loading…
-
- )}
-
-
- );
-}
-function NoRuns({ title }: { title: string }) {
- return (
-
- );
-}
diff --git a/apps/webapp/app/routes/resources.$organizationSlug.subscribe.ts b/apps/webapp/app/routes/resources.$organizationSlug.subscribe.ts
deleted file mode 100644
index cc67dea118..0000000000
--- a/apps/webapp/app/routes/resources.$organizationSlug.subscribe.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { parse } from "@conform-to/zod";
-import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { SetPlanBodySchema } from "@trigger.dev/platform/v2";
-import { redirect } from "remix-typedjson";
-import { prisma } from "~/db.server";
-import { redirectWithSuccessMessage } from "~/models/message.server";
-import { BillingService } from "~/services/billing.v2.server";
-import { logger } from "~/services/logger.server";
-import { requireUserId } from "~/services/session.server";
-import {
- OrganizationParamsSchema,
- organizationBillingPath,
- subscribedPath,
-} from "~/utils/pathBuilder";
-
-export async function action({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
-
- const { organizationSlug } = OrganizationParamsSchema.parse(params);
-
- const formData = await request.formData();
- const submission = parse(formData, { schema: SetPlanBodySchema });
- if (!submission.value || submission.intent !== "submit") {
- return json(submission);
- }
-
- try {
- const org = await prisma.organization.findUnique({
- select: {
- id: true,
- },
- where: {
- slug: organizationSlug,
- members: {
- some: {
- userId,
- },
- },
- },
- });
-
- if (!org) {
- submission.error.message = "Invalid organization";
- return json(submission);
- }
-
- const billingPresenter = new BillingService(true);
- const result = await billingPresenter.setPlan(org.id, submission.value);
- if (result === undefined) {
- submission.error.message = "No billing client";
- return json(submission);
- }
-
- if (!result.success) {
- submission.error.message = result.error;
- return json(submission);
- }
-
- switch (result.action) {
- case "create_subscription_flow_start": {
- return redirect(result.checkoutUrl);
- }
- case "canceled_subscription": {
- return redirectWithSuccessMessage(
- organizationBillingPath({ slug: organizationSlug }),
- request,
- "Your subscription has been canceled."
- );
- }
- case "updated_subscription": {
- return redirect(subscribedPath({ slug: organizationSlug }), request);
- }
- }
- } catch (e) {
- logger.error("Error setting plan", { error: e });
- submission.error.message = e instanceof Error ? e.message : JSON.stringify(e);
- return json(submission);
- }
-}
diff --git a/apps/webapp/app/routes/resources.$organizationSlug.subscription.portal.ts b/apps/webapp/app/routes/resources.$organizationSlug.subscription.portal.ts
deleted file mode 100644
index ab2865221d..0000000000
--- a/apps/webapp/app/routes/resources.$organizationSlug.subscription.portal.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { ActionFunctionArgs } from "@remix-run/server-runtime";
-import { redirect } from "remix-typedjson";
-import { prisma } from "~/db.server";
-import { redirectWithErrorMessage } from "~/models/message.server";
-import { BillingService } from "~/services/billing.v2.server";
-import { requireUserId } from "~/services/session.server";
-import { OrganizationParamsSchema, usagePath } from "~/utils/pathBuilder";
-
-export async function loader({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
- const { organizationSlug } = OrganizationParamsSchema.parse(params);
-
- const org = await prisma.organization.findUnique({
- select: {
- id: true,
- },
- where: {
- slug: organizationSlug,
- members: {
- some: {
- userId,
- },
- },
- },
- });
-
- if (!org) {
- return redirectWithErrorMessage(
- usagePath({ slug: organizationSlug }),
- request,
- "Something went wrong. Please try again later."
- );
- }
-
- const billingPresenter = new BillingService(true);
- const result = await billingPresenter.customerPortalUrl(org.id, organizationSlug);
-
- if (!result || !result.success || !result.customerPortalUrl) {
- return redirectWithErrorMessage(
- usagePath({ slug: organizationSlug }),
- request,
- "Something went wrong. Please try again later."
- );
- }
-
- return redirect(result.customerPortalUrl);
-}
diff --git a/apps/webapp/app/routes/resources.apivote.$identifier.ts b/apps/webapp/app/routes/resources.apivote.$identifier.ts
deleted file mode 100644
index 1a471f6e50..0000000000
--- a/apps/webapp/app/routes/resources.apivote.$identifier.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { z } from "zod";
-import { ApiVoteService } from "~/services/apiVote.server";
-import { requireUserId } from "~/services/session.server";
-
-const ParamsSchema = z.object({
- identifier: z.string(),
-});
-
-export async function action({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
- const { identifier } = ParamsSchema.parse(params);
-
- const service = new ApiVoteService();
-
- try {
- const result = await service.call({ userId, identifier });
- return json(result);
- } catch (e) {
- return json(e, { status: 400 });
- }
-}
diff --git a/apps/webapp/app/routes/resources.codeexample.tsx b/apps/webapp/app/routes/resources.codeexample.tsx
deleted file mode 100644
index 04fca07e0f..0000000000
--- a/apps/webapp/app/routes/resources.codeexample.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useFetcher } from "@remix-run/react";
-import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
-import { useEffect } from "react";
-import invariant from "tiny-invariant";
-import { CodeBlock } from "~/components/code/CodeBlock";
-import { Paragraph } from "~/components/primitives/Paragraph";
-import { Spinner } from "~/components/primitives/Spinner";
-import { ApiExample } from "~/services/externalApis/apis.server";
-import { requireUserId } from "~/services/session.server";
-
-export async function loader({ request }: LoaderFunctionArgs) {
- await requireUserId(request);
- const url = new URL(request.url);
- const codeUrl = url.searchParams.get("url");
- invariant(typeof codeUrl === "string", "codeUrl is required");
- const decodedCodeUrl = decodeURIComponent(codeUrl);
- const response = await fetch(decodedCodeUrl);
- if (!response.ok) {
- throw new Error("Network response was not ok");
- }
-
- const code = await response.text();
-
- const hideCodeRegex = /(\n)?\/\/ hide-code[\s\S]*?\/\/ end-hide-code(\n)*/gm;
- const cleanedCode = code?.replace(hideCodeRegex, "\n");
-
- return json({
- code: cleanedCode,
- });
-}
-
-export function CodeExample({ example }: { example: ApiExample }) {
- const customerFetcher = useFetcher();
-
- useEffect(() => {
- customerFetcher.load(`/resources/codeexample?url=${encodeURIComponent(example.codeUrl)}`);
- }, [example.codeUrl]);
-
- if (customerFetcher.state === "loading")
- return (
-
-
-
Loading example code
-
- );
-
- return (
- customerFetcher.data &&
- );
-}
diff --git a/apps/webapp/app/routes/resources.connection.$organizationId.oauth2.$integrationId.ts b/apps/webapp/app/routes/resources.connection.$organizationId.oauth2.$integrationId.ts
deleted file mode 100644
index 396b8f5757..0000000000
--- a/apps/webapp/app/routes/resources.connection.$organizationId.oauth2.$integrationId.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import { conform } from "@conform-to/react";
-import { parse } from "@conform-to/zod";
-import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { redirect, typedjson } from "remix-typedjson";
-import z from "zod";
-import { prisma } from "~/db.server";
-import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
-import { requireUserId } from "~/services/session.server";
-import { requestUrl } from "~/utils/requestUrl.server";
-
-export const schema = z
- .object({
- integrationIdentifier: z.string(),
- integrationAuthMethod: z.string(),
- title: z.string().min(2, "The title must be unique and at least 2 characters long"),
- description: z.string().optional(),
- hasCustomClient: z.preprocess((value) => value === "on", z.boolean()),
- customClientId: z.string().optional(),
- customClientSecret: z.string().optional(),
- clientType: z.union([z.literal("DEVELOPER"), z.literal("EXTERNAL")]),
- redirectTo: z.string(),
- scopes: z.preprocess(
- (data) => (typeof data === "string" ? [data] : data),
- z
- .array(z.string(), {
- required_error: "You must select at least one scope",
- })
- .nonempty("You must select at least one scope")
- ),
- })
- .refine(
- (value) => {
- if (value.hasCustomClient) {
- return (
- value.customClientId !== undefined &&
- value.customClientId !== "" &&
- value.customClientSecret !== undefined &&
- value.customClientSecret !== ""
- );
- }
- return true;
- },
- {
- message:
- "You must enter a Client ID and Client secret if you want to use your own OAuth app.",
- path: ["customClientId"],
- }
- );
-
-const ParamsSchema = z.object({
- organizationId: z.string(),
- integrationId: z.string(),
-});
-
-export async function action({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
-
- if (request.method.toUpperCase() !== "PUT") {
- return typedjson(
- {
- type: "error" as const,
- error: "Method not allowed",
- },
- { status: 405 }
- );
- }
- const { integrationId, organizationId } = ParamsSchema.parse(params);
-
- const formData = await request.formData();
- const submission = parse(formData, { schema });
-
- if (!submission.value || submission.intent !== "submit") {
- return json(submission);
- }
-
- const {
- hasCustomClient,
- customClientId,
- customClientSecret,
- integrationIdentifier,
- integrationAuthMethod,
- clientType,
- title,
- description,
- redirectTo,
- scopes,
- } = submission.value;
-
- const organization = await prisma.organization.findFirstOrThrow({
- where: {
- id: organizationId,
- members: {
- some: {
- userId,
- },
- },
- },
- });
-
- const url = requestUrl(request);
-
- const redirectUrl = await integrationAuthRepository.populateMissingConnectionClientFields({
- id: integrationId,
- customClient: hasCustomClient
- ? { id: customClientId!, secret: customClientSecret! }
- : undefined,
- clientType,
- organizationId: organization.id,
- integrationIdentifier,
- integrationAuthMethod,
- scopes,
- title,
- description,
- redirectTo,
- url,
- });
-
- return redirect(redirectUrl);
-}
diff --git a/apps/webapp/app/routes/resources.connection.$organizationId.oauth2.ts b/apps/webapp/app/routes/resources.connection.$organizationId.oauth2.ts
deleted file mode 100644
index a0d6bb1328..0000000000
--- a/apps/webapp/app/routes/resources.connection.$organizationId.oauth2.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import { conform } from "@conform-to/react";
-import { parse } from "@conform-to/zod";
-import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { redirect, typedjson } from "remix-typedjson";
-import z from "zod";
-import { prisma } from "~/db.server";
-import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
-import { requireUserId } from "~/services/session.server";
-import { requestUrl } from "~/utils/requestUrl.server";
-
-export function createSchema(
- constraints: {
- isTitleUnique?: (title: string) => Promise;
- isSlugUnique?: (slug: string) => Promise;
- } = {}
-) {
- return z
- .object({
- id: z.string(),
- slug: z
- .string()
- .min(2, "The id must be at least 2 characters long")
- .superRefine((title, ctx) => {
- if (constraints.isSlugUnique === undefined) {
- //client-side validation skips this
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: conform.VALIDATION_UNDEFINED,
- });
- } else {
- // Tell zod this is an async validation by returning the promise
- return constraints.isSlugUnique(title).then((isUnique) => {
- if (isUnique) {
- return;
- }
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "The id must be unique in your organization",
- });
- });
- }
- }),
- integrationIdentifier: z.string(),
- integrationAuthMethod: z.string(),
- title: z
- .string()
- .min(2, "The title must be unique and at least 2 characters long")
- .superRefine((title, ctx) => {
- if (constraints.isTitleUnique === undefined) {
- //client-side validation skips this
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: conform.VALIDATION_UNDEFINED,
- });
- } else {
- // Tell zod this is an async validation by returning the promise
- return constraints.isTitleUnique(title).then((isUnique) => {
- if (isUnique) {
- return;
- }
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "The title must be unique in your organization",
- });
- });
- }
- }),
- description: z.string().optional(),
- hasCustomClient: z.preprocess((value) => value === "on", z.boolean()),
- customClientId: z.string().optional(),
- customClientSecret: z.string().optional(),
- clientType: z.union([z.literal("DEVELOPER"), z.literal("EXTERNAL")]),
- redirectTo: z.string(),
- scopes: z.preprocess(
- (data) => (typeof data === "string" ? [data] : data),
- z.array(z.string()).default([])
- ),
- })
- .refine(
- (value) => {
- if (value.hasCustomClient) {
- return (
- value.customClientId !== undefined &&
- value.customClientId !== "" &&
- value.customClientSecret !== undefined &&
- value.customClientSecret !== ""
- );
- }
- return true;
- },
- {
- message:
- "You must enter a Client ID and Client secret if you want to use your own OAuth app.",
- path: ["customClientId"],
- }
- );
-}
-
-const ParamsSchema = z.object({
- organizationId: z.string(),
-});
-
-export async function action({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
-
- if (request.method.toUpperCase() !== "POST") {
- return typedjson(
- {
- type: "error" as const,
- error: "Method not allowed",
- },
- { status: 405 }
- );
- }
- const { organizationId } = ParamsSchema.parse(params);
-
- const formData = await request.formData();
-
- const formSchema = createSchema({
- isTitleUnique: async (title) => {
- const existingIntegration = await prisma.integration.findFirst({
- where: {
- organizationId,
- title,
- },
- });
-
- return !existingIntegration;
- },
- isSlugUnique: async (slug) => {
- const existingIntegration = await prisma.integration.findFirst({
- where: {
- organizationId,
- slug,
- },
- });
-
- return !existingIntegration;
- },
- });
-
- const submission = await parse(formData, { schema: formSchema, async: true });
-
- if (!submission.value || submission.intent !== "submit") {
- return json(submission);
- }
-
- const {
- id,
- slug,
- hasCustomClient,
- customClientId,
- customClientSecret,
- integrationIdentifier,
- integrationAuthMethod,
- clientType,
- title,
- description,
- redirectTo,
- scopes,
- } = submission.value;
-
- const organization = await prisma.organization.findFirstOrThrow({
- where: {
- id: organizationId,
- members: {
- some: {
- userId,
- },
- },
- },
- });
-
- const url = requestUrl(request);
-
- const redirectUrl = await integrationAuthRepository.createConnectionClient({
- id,
- slug,
- customClient: hasCustomClient
- ? { id: customClientId!, secret: customClientSecret! }
- : undefined,
- clientType,
- organizationId: organization.id,
- integrationIdentifier,
- integrationAuthMethod,
- scopes,
- title,
- description,
- redirectTo,
- url,
- });
-
- return redirect(redirectUrl);
-}
diff --git a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts
deleted file mode 100644
index f4483e331f..0000000000
--- a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { z } from "zod";
-import { DeleteEndpointService } from "~/services/endpoints/deleteEndpointService";
-import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
-import { requireUserId } from "~/services/session.server";
-import { workerQueue } from "~/services/worker.server";
-
-const ParamsSchema = z.object({
- environmentParam: z.string(),
- endpointParam: z.string(),
-});
-
-const BodySchema = z.discriminatedUnion("action", [
- z.object({ action: z.literal("refresh") }),
- z.object({ action: z.literal("delete") }),
-]);
-
-export async function action({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
- if (request.method !== "POST") {
- throw new Response(null, { status: 405 });
- }
-
- try {
- const { endpointParam } = ParamsSchema.parse(params);
- const form = await request.formData();
- const formObject = Object.fromEntries(form.entries());
- const { action } = BodySchema.parse(formObject);
-
- switch (action) {
- case "refresh": {
- const service = new IndexEndpointService();
- await service.call(endpointParam, "MANUAL");
-
- // Enqueue the endpoint to be probed in 10 seconds
- await workerQueue.enqueue(
- "probeEndpoint",
- { id: endpointParam },
- { jobKey: `probe:${endpointParam}`, runAt: new Date(Date.now() + 10000) }
- );
-
- return json({ success: true });
- }
- case "delete": {
- const service = new DeleteEndpointService();
- await service.call(endpointParam, userId);
- return json({ success: true });
- }
- }
- } catch (e) {
- return json({ success: false, error: e }, { status: 400 });
- }
-}
diff --git a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.stream.ts b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.stream.ts
deleted file mode 100644
index 88d20e7373..0000000000
--- a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.stream.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { LoaderFunctionArgs } from "@remix-run/server-runtime";
-import { z } from "zod";
-import { prisma } from "~/db.server";
-import { requireUserId } from "~/services/session.server";
-import { sse } from "~/utils/sse.server";
-
-export async function loader({ request, params }: LoaderFunctionArgs) {
- await requireUserId(request);
-
- const { environmentParam } = z.object({ environmentParam: z.string() }).parse(params);
-
- const environment = await environmentForUpdates(environmentParam);
-
- if (!environment) {
- return new Response("Not found", { status: 404 });
- }
-
- let lastSignals = calculateChangeSignals(environment);
-
- return sse({
- request,
- run: async (send, stop) => {
- const result = await environmentForUpdates(environmentParam);
- if (!result) {
- return stop();
- }
-
- const newSignals = calculateChangeSignals(result);
-
- if (lastSignals.lastUpdatedAt !== result.updatedAt.getTime()) {
- send({ data: result.updatedAt.toISOString() });
- } else if (
- lastSignals.lastTotalEndpointUpdatedTime !== newSignals.lastTotalEndpointUpdatedTime
- ) {
- send({ data: new Date().toISOString() });
- } else if (
- lastSignals.lastTotalIndexingUpdatedTime !== newSignals.lastTotalIndexingUpdatedTime
- ) {
- send({ data: new Date().toISOString() });
- }
-
- lastSignals = newSignals;
- },
- });
-}
-
-function environmentForUpdates(id: string) {
- return prisma.runtimeEnvironment.findUnique({
- where: {
- id,
- },
- select: {
- updatedAt: true,
- endpoints: {
- select: {
- updatedAt: true,
- indexings: {
- select: {
- updatedAt: true,
- },
- },
- },
- },
- },
- });
-}
-
-function calculateChangeSignals(
- environment: NonNullable>>
-) {
- let lastUpdatedAt: number = environment.updatedAt.getTime();
- let lastTotalEndpointUpdatedTime = environment.endpoints.reduce(
- (prev, endpoint) => prev + endpoint.updatedAt.getTime(),
- 0
- );
- let lastTotalIndexingUpdatedTime = environment.endpoints.reduce(
- (prev, endpoint) =>
- prev + endpoint.indexings.reduce((prev, indexing) => prev + indexing.updatedAt.getTime(), 0),
- 0
- );
-
- return {
- lastUpdatedAt,
- lastTotalEndpointUpdatedTime,
- lastTotalIndexingUpdatedTime,
- };
-}
diff --git a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.ts b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.ts
deleted file mode 100644
index c95ce9c3cc..0000000000
--- a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { parse } from "@conform-to/zod";
-import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { z } from "zod";
-import { prisma } from "~/db.server";
-import {
- CreateEndpointError,
- CreateEndpointService,
-} from "~/services/endpoints/createEndpoint.server";
-import { requireUserId } from "~/services/session.server";
-
-const ParamsSchema = z.object({
- environmentParam: z.string(),
-});
-
-export const bodySchema = z.object({
- clientSlug: z.string(),
- url: z.string().url("Must be a valid URL"),
-});
-
-export async function action({ request, params }: ActionFunctionArgs) {
- const userId = await requireUserId(request);
- const { environmentParam } = ParamsSchema.parse(params);
-
- const formData = await request.formData();
- const submission = parse(formData, { schema: bodySchema });
-
- if (!submission.value || submission.intent !== "submit") {
- return json(submission);
- }
-
- try {
- const environment = await prisma.runtimeEnvironment.findUnique({
- include: {
- organization: true,
- project: true,
- },
- where: {
- id: environmentParam,
- },
- });
-
- if (!environment) {
- throw new Error("Environment not found");
- }
-
- const service = new CreateEndpointService();
- const result = await service.call({
- id: submission.value.clientSlug,
- url: submission.value.url,
- environment,
- });
-
- return json(submission);
- } catch (e) {
- if (e instanceof CreateEndpointError) {
- submission.error.url = e.message;
- return json(submission);
- }
-
- if (e instanceof Error) {
- submission.error.url = `${e.name}: ${e.message}`;
- } else {
- submission.error.url = "Unknown error";
- }
-
- return json(submission, { status: 400 });
- }
-}
diff --git a/apps/webapp/app/routes/resources.jobs.$jobId.ts b/apps/webapp/app/routes/resources.jobs.$jobId.ts
deleted file mode 100644
index e73c3c6893..0000000000
--- a/apps/webapp/app/routes/resources.jobs.$jobId.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { ActionFunction, LoaderFunction, LoaderFunctionArgs, json } from "@remix-run/node";
-import { typedjson } from "remix-typedjson";
-import { z } from "zod";
-import { prisma } from "~/db.server";
-import {
- jsonWithErrorMessage,
- jsonWithSuccessMessage,
- redirectWithSuccessMessage,
-} from "~/models/message.server";
-import { DeleteJobService } from "~/services/jobs/deleteJob.server";
-import { logger } from "~/services/logger.server";
-import { requireUserId } from "~/services/session.server";
-
-const ParamSchema = z.object({
- jobId: z.string(),
-});
-
-export async function loader({ request, params }: LoaderFunctionArgs) {
- const userId = await requireUserId(request);
- const { jobId } = ParamSchema.parse(params);
-
- const job = await prisma.job.findFirst({
- select: {
- id: true,
- slug: true,
- title: true,
- aliases: {
- select: {
- version: {
- select: {
- version: true,
- status: true,
- concurrencyLimit: true,
- concurrencyLimitGroup: {
- select: {
- name: true,
- concurrencyLimit: true,
- },
- },
- runs: {
- select: {
- createdAt: true,
- status: true,
- },
- take: 1,
- orderBy: [{ createdAt: "desc" }],
- },
- },
- },
- environment: {
- select: {
- type: true,
- orgMember: {
- select: {
- userId: true,
- },
- },
- },
- },
- },
- where: {
- name: "latest",
- },
- },
- },
- where: {
- id: jobId,
- deletedAt: null,
- organization: {
- members: {
- some: {
- userId,
- },
- },
- },
- },
- });
-
- if (!job) {
- throw new Response("Not Found", { status: 404 });
- }
-
- const environments = job.aliases.map((alias) => ({
- type: alias.environment.type,
- enabled: alias.version.status === "ACTIVE",
- lastRun: alias.version.runs.at(0)?.createdAt,
- version: alias.version.version,
- concurrencyLimit: alias.version.concurrencyLimit,
- concurrencyLimitGroup: alias.version.concurrencyLimitGroup,
- }));
-
- return typedjson({
- environments,
- });
-}
-
-export const action: ActionFunction = async ({ request, params }) => {
- if (request.method.toUpperCase() !== "DELETE") {
- return { status: 405, body: "Method Not Allowed" };
- }
-
- const { jobId } = ParamSchema.parse(params);
- const userId = await requireUserId(request);
-
- // Find the job
- const job = await prisma.job.findFirst({
- where: {
- id: jobId,
- organization: {
- members: {
- some: {
- userId,
- },
- },
- },
- },
- });
-
- if (!job) {
- return jsonWithErrorMessage({ ok: false }, request, `Job could not be scheduled for deletion.`);
- }
- try {
- const deleteJobService = new DeleteJobService();
-
- await deleteJobService.call(job);
-
- const url = new URL(request.url);
- const redirectTo = url.searchParams.get("redirectTo");
-
- logger.debug("Job scheduled for deletion", {
- url,
- redirectTo,
- job,
- });
-
- if (typeof redirectTo === "string" && redirectTo.length > 0) {
- return redirectWithSuccessMessage(
- redirectTo,
- request,
- `Job ${job.slug} has been scheduled for deletion.`
- );
- }
-
- return jsonWithSuccessMessage(
- { ok: true },
- request,
- `Job ${job.slug} has been scheduled for deletion.`
- );
- } catch (error) {
- const message = error instanceof Error ? error.message : "Unknown error";
-
- return jsonWithErrorMessage(
- { ok: false },
- request,
- `Job could not be scheduled for deletion: ${message}`
- );
- }
-};
diff --git a/apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts b/apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts
deleted file mode 100644
index f8f9fd7b60..0000000000
--- a/apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { LoaderFunctionArgs } from "@remix-run/server-runtime";
-import { z } from "zod";
-import { prisma } from "~/db.server";
-import { requireUserId } from "~/services/session.server";
-import { sse } from "~/utils/sse.server";
-
-export async function loader({ request, params }: LoaderFunctionArgs) {
- await requireUserId(request);
-
- const { projectId } = z.object({ projectId: z.string() }).parse(params);
-
- const project = await projectForUpdates(projectId);
-
- if (!project) {
- return new Response("Not found", { status: 404 });
- }
-
- let lastSignals = calculateChangeSignals(project);
-
- return sse({
- request,
- run: async (send, stop) => {
- const result = await projectForUpdates(projectId);
- if (!result) {
- return stop();
- }
-
- const newSignals = calculateChangeSignals(result);
-
- if (lastSignals.jobCount !== newSignals.jobCount) {
- send({ data: JSON.stringify(newSignals) });
- }
-
- lastSignals = newSignals;
- },
- });
-}
-
-function projectForUpdates(id: string) {
- return prisma.project.findFirst({
- where: {
- id,
- },
- include: {
- _count: {
- select: { jobs: true },
- },
- },
- });
-}
-
-function calculateChangeSignals(
- project: NonNullable>>
-) {
- const jobCount = project._count?.jobs ?? 0;
-
- return {
- jobCount,
- };
-}
diff --git a/apps/webapp/app/routes/resources.runs.$runId.cancel.ts b/apps/webapp/app/routes/resources.runs.$runId.cancel.ts
deleted file mode 100644
index af475db065..0000000000
--- a/apps/webapp/app/routes/resources.runs.$runId.cancel.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { parse } from "@conform-to/zod";
-import { ActionFunction, json } from "@remix-run/node";
-import { z } from "zod";
-import { redirectWithSuccessMessage } from "~/models/message.server";
-import { logger } from "~/services/logger.server";
-import { CancelRunService } from "~/services/runs/cancelRun.server";
-
-export const cancelSchema = z.object({
- redirectUrl: z.string(),
-});
-
-const ParamSchema = z.object({
- runId: z.string(),
-});
-
-export const action: ActionFunction = async ({ request, params }) => {
- const { runId } = ParamSchema.parse(params);
-
- const formData = await request.formData();
- const submission = parse(formData, { schema: cancelSchema });
-
- if (!submission.value) {
- return json(submission);
- }
-
- try {
- const cancelRunService = new CancelRunService();
- await cancelRunService.call({ runId });
-
- return redirectWithSuccessMessage(
- submission.value.redirectUrl,
- request,
- `Canceled run. Any pending tasks will be canceled.`
- );
- } catch (error) {
- if (error instanceof Error) {
- logger.error("Failed to cancel run", {
- error: {
- name: error.name,
- message: error.message,
- stack: error.stack,
- },
- });
- return json({ errors: { body: error.message } }, { status: 400 });
- } else {
- logger.error("Failed to cancel run", { error });
- return json({ errors: { body: "Unknown error" } }, { status: 400 });
- }
- }
-};
diff --git a/apps/webapp/app/routes/resources.runs.$runId.rerun.ts b/apps/webapp/app/routes/resources.runs.$runId.rerun.ts
deleted file mode 100644
index c7a2e6a743..0000000000
--- a/apps/webapp/app/routes/resources.runs.$runId.rerun.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { parse } from "@conform-to/zod";
-import { ActionFunction, json } from "@remix-run/node";
-import { z } from "zod";
-import {
- redirectBackWithErrorMessage,
- redirectWithErrorMessage,
- redirectWithSuccessMessage,
-} from "~/models/message.server";
-import { ContinueRunService } from "~/services/runs/continueRun.server";
-import { ReRunService } from "~/services/runs/reRun.server";
-import { rootPath, runPath } from "~/utils/pathBuilder";
-
-export const schema = z.object({
- successRedirect: z.string(),
- failureRedirect: z.string(),
-});
-
-const ParamSchema = z.object({
- runId: z.string(),
-});
-
-export const action: ActionFunction = async ({ request, params }) => {
- const { runId } = ParamSchema.parse(params);
-
- const formData = await request.formData();
- const submission = parse(formData, { schema });
-
- if (!submission.value) {
- return redirectWithErrorMessage(
- rootPath(),
- request,
- submission.error ? JSON.stringify(submission.error) : "Invalid form"
- );
- }
-
- try {
- if (submission.intent === "start") {
- const rerunService = new ReRunService();
- const run = await rerunService.call({ runId });
-
- if (!run) {
- return redirectWithErrorMessage(
- submission.value.failureRedirect,
- request,
- "Unable to retry run"
- );
- }
-
- return redirectWithSuccessMessage(
- `${submission.value.successRedirect}/${run.id}`,
- request,
- `Created new run`
- );
- } else if (submission.intent === "continue") {
- const continueService = new ContinueRunService();
- await continueService.call({ runId });
-
- return redirectWithSuccessMessage(
- `${submission.value.successRedirect}/${runId}`,
- request,
- `Resuming run`
- );
- }
- } catch (error: any) {
- return redirectWithErrorMessage(
- submission.value.failureRedirect,
- request,
- error instanceof Error ? error.message : JSON.stringify(error)
- );
- }
-};
diff --git a/apps/webapp/app/routes/storybook.input-fields/route.tsx b/apps/webapp/app/routes/storybook.input-fields/route.tsx
index 322b816b65..7049356546 100644
--- a/apps/webapp/app/routes/storybook.input-fields/route.tsx
+++ b/apps/webapp/app/routes/storybook.input-fields/route.tsx
@@ -1,7 +1,5 @@
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Input } from "~/components/primitives/Input";
-import { InputGroup } from "~/components/primitives/InputGroup";
-import { TimeFrameFilter } from "~/components/runs/TimeFrameFilter";
export default function Story() {
return (
diff --git a/apps/webapp/app/services/apiVote.server.ts b/apps/webapp/app/services/apiVote.server.ts
deleted file mode 100644
index e841a31d6d..0000000000
--- a/apps/webapp/app/services/apiVote.server.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { PrismaClient, prisma } from "~/db.server";
-
-export class ApiVoteService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call({ userId, identifier }: { userId: string; identifier: string }) {
- return this.#prismaClient.apiIntegrationVote.create({
- data: {
- user: {
- connect: {
- id: userId,
- },
- },
- apiIdentifier: identifier,
- },
- });
- }
-}
diff --git a/apps/webapp/app/services/billing.v2.server.ts b/apps/webapp/app/services/billing.v2.server.ts
deleted file mode 100644
index 43121b5b56..0000000000
--- a/apps/webapp/app/services/billing.v2.server.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { BillingClient, SetPlanBody } from "@trigger.dev/platform/v2";
-import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
-import { env } from "~/env.server";
-import { logger } from "~/services/logger.server";
-import { organizationBillingPath } from "~/utils/pathBuilder";
-
-export class BillingService {
- #billingClient: BillingClient | undefined;
- #prismaClient: PrismaClient;
- #replica: PrismaReplicaClient;
-
- constructor(
- isManagedCloud: boolean,
- prismaClient: PrismaClient = prisma,
- replica: PrismaReplicaClient = $replica
- ) {
- this.#prismaClient = prismaClient;
- this.#replica = replica;
- if (isManagedCloud && process.env.BILLING_API_URL && process.env.BILLING_API_KEY) {
- this.#billingClient = new BillingClient({
- url: process.env.BILLING_API_URL,
- apiKey: process.env.BILLING_API_KEY,
- });
- console.log(`Billing client initialized: ${process.env.BILLING_API_URL}`);
- } else {
- console.log(`Billing client not initialized`);
- }
- }
-
- async currentPlan(orgId: string) {
- if (!this.#billingClient) return undefined;
- try {
- const result = await this.#billingClient.currentPlan(orgId);
-
- const firstDayOfMonth = new Date();
- firstDayOfMonth.setUTCDate(1);
- firstDayOfMonth.setUTCHours(0, 0, 0, 0);
-
- const firstDayOfNextMonth = new Date();
- firstDayOfNextMonth.setUTCDate(1);
- firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);
- firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);
-
- const currentRunCount = await this.#replica.jobRun.count({
- where: {
- organizationId: orgId,
- createdAt: {
- gte: firstDayOfMonth,
- },
- },
- });
-
- if (!result.success) {
- logger.error("Error getting current plan", { orgId, error: result.error });
- return undefined;
- }
-
- const periodStart = firstDayOfMonth;
- const periodEnd = firstDayOfNextMonth;
- const periodRemainingDuration = periodEnd.getTime() - new Date().getTime();
-
- const usage = {
- currentRunCount,
- runCountCap: result.subscription?.plan.runs?.freeAllowance,
- exceededRunCount: result.subscription?.plan.runs?.freeAllowance
- ? currentRunCount > result.subscription?.plan.runs?.freeAllowance
- : false,
- periodStart,
- periodEnd,
- periodRemainingDuration,
- };
-
- return { ...result, usage };
- } catch (e) {
- logger.error("Error getting current plan", { orgId, error: e });
- return undefined;
- }
- }
-
- async customerPortalUrl(orgId: string, orgSlug: string) {
- if (!this.#billingClient) return undefined;
- try {
- return this.#billingClient.createPortalSession(orgId, {
- returnUrl: `${env.APP_ORIGIN}${organizationBillingPath({ slug: orgSlug })}`,
- });
- } catch (e) {
- logger.error("Error getting customer portal Url", { orgId, error: e });
- return undefined;
- }
- }
-
- async getPlans() {
- if (!this.#billingClient) return undefined;
- try {
- const result = await this.#billingClient.plans();
- if (!result.success) {
- logger.error("Error getting plans", { error: result.error });
- return undefined;
- }
- return result;
- } catch (e) {
- logger.error("Error getting plans", { error: e });
- return undefined;
- }
- }
-
- async setPlan(orgId: string, plan: SetPlanBody) {
- if (!this.#billingClient) return undefined;
- try {
- const result = await this.#billingClient.setPlan(orgId, plan);
- return result;
- } catch (e) {
- logger.error("Error setting plan", { orgId, error: e });
- return undefined;
- }
- }
-}
diff --git a/apps/webapp/app/services/dispatchers/createEphemeralEventDispatcher.server.ts b/apps/webapp/app/services/dispatchers/createEphemeralEventDispatcher.server.ts
deleted file mode 100644
index 7d6cb822ed..0000000000
--- a/apps/webapp/app/services/dispatchers/createEphemeralEventDispatcher.server.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { EphemeralEventDispatcherRequestBody } from "@trigger.dev/core";
-import { $transaction, PrismaClient, prisma } from "~/db.server";
-import { AuthenticatedEnvironment } from "../apiAuth.server";
-import { ExpireDispatcherService } from "./expireDispatcher.server";
-
-export class CreateEphemeralEventDispatcherService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(
- environment: AuthenticatedEnvironment,
- data: EphemeralEventDispatcherRequestBody
- ) {
- return await $transaction(this.#prismaClient, async (tx) => {
- const existingDispatcher = await tx.eventDispatcher.findUnique({
- where: {
- dispatchableId_environmentId: {
- dispatchableId: data.url,
- environmentId: environment.id,
- },
- },
- });
-
- if (existingDispatcher) {
- return existingDispatcher;
- }
-
- const externalAccount = data.accountId
- ? await this.#prismaClient.externalAccount.upsert({
- where: {
- environmentId_identifier: {
- environmentId: environment.id,
- identifier: data.accountId,
- },
- },
- create: {
- environmentId: environment.id,
- organizationId: environment.organizationId,
- identifier: data.accountId,
- },
- update: {},
- })
- : undefined;
-
- const dispatcher = await tx.eventDispatcher.create({
- data: {
- dispatchableId: data.url,
- environmentId: environment.id,
- source: data.source ?? "trigger.dev",
- payloadFilter: data.filter,
- contextFilter: data.contextFilter,
- dispatchable: { url: data.url, type: "EPHEMERAL" },
- enabled: true,
- event: typeof data.name === "string" ? [data.name] : data.name,
- manual: false,
- externalAccountId: externalAccount?.id,
- },
- });
-
- await ExpireDispatcherService.enqueue(dispatcher.id, data.timeoutInSeconds, tx);
-
- return dispatcher;
- });
- }
-}
diff --git a/apps/webapp/app/services/dispatchers/expireDispatcher.server.ts b/apps/webapp/app/services/dispatchers/expireDispatcher.server.ts
deleted file mode 100644
index 85032a639c..0000000000
--- a/apps/webapp/app/services/dispatchers/expireDispatcher.server.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
-import { workerQueue } from "../worker.server";
-
-export class ExpireDispatcherService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string) {
- await this.#prismaClient.eventDispatcher.delete({
- where: {
- id,
- },
- });
- }
-
- static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
- await workerQueue.dequeue(`expire:${id}`, { tx });
- }
-
- static async enqueue(id: string, timeoutInSeconds: number, tx?: PrismaClientOrTransaction) {
- await workerQueue.enqueue(
- "expireDispatcher",
- {
- id,
- },
- {
- tx,
- runAt: new Date(Date.now() + 1000 * timeoutInSeconds),
- jobKey: `expire:${id}`,
- }
- );
- }
-}
diff --git a/apps/webapp/app/services/dispatchers/invokeEphemeralEventDispatcher.server.ts b/apps/webapp/app/services/dispatchers/invokeEphemeralEventDispatcher.server.ts
deleted file mode 100644
index ce96c5d4a9..0000000000
--- a/apps/webapp/app/services/dispatchers/invokeEphemeralEventDispatcher.server.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
-import { taskOperationWorker } from "../worker.server";
-import { EphemeralDispatchableSchema } from "~/models/eventDispatcher.server";
-import { ExpireDispatcherService } from "./expireDispatcher.server";
-
-export class InvokeEphemeralDispatcherService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string, eventRecordId: string) {
- const eventDispatcher = await this.#prismaClient.eventDispatcher.findUnique({
- where: {
- id,
- },
- });
-
- if (!eventDispatcher) {
- return;
- }
-
- if (!eventDispatcher.enabled) {
- return;
- }
-
- const eventRecord = await this.#prismaClient.eventRecord.findUnique({
- where: {
- id: eventRecordId,
- },
- include: {
- externalAccount: true,
- },
- });
-
- if (!eventRecord) {
- return;
- }
-
- if (eventRecord.cancelledAt) {
- return;
- }
-
- const dispatchable = EphemeralDispatchableSchema.safeParse(eventDispatcher.dispatchable);
-
- if (!dispatchable.success) {
- return;
- }
-
- const url = dispatchable.data.url;
-
- const body = {
- id: eventRecord.eventId,
- source: eventRecord.source,
- name: eventRecord.name,
- payload: eventRecord.payload,
- context: eventRecord.context,
- timestamp: eventRecord.timestamp,
- accountId: eventRecord.externalAccount ? eventRecord.externalAccount.identifier : undefined,
- };
-
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json; charset=utf-8",
- },
- body: JSON.stringify(body),
- });
-
- if (!response.ok) {
- throw new Error(
- `Failed to invoke ephemeral dispatcher: ${response.statusText} [${response.status}]`
- );
- }
-
- // Run the expire dispatcher service
- await ExpireDispatcherService.enqueue(id, 0);
- }
-
- static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
- await taskOperationWorker.dequeue(`invoke:ephemeral:${id}`, { tx });
- }
-
- static async enqueue(id: string, eventRecordId: string, tx?: PrismaClientOrTransaction) {
- await taskOperationWorker.enqueue(
- "invokeEphemeralDispatcher",
- {
- id,
- eventRecordId,
- },
- {
- tx,
- jobKey: `invoke:ephemeral:${id}`,
- }
- );
- }
-}
diff --git a/apps/webapp/app/services/endpoints/createEndpoint.server.ts b/apps/webapp/app/services/endpoints/createEndpoint.server.ts
deleted file mode 100644
index 76a4418faa..0000000000
--- a/apps/webapp/app/services/endpoints/createEndpoint.server.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { customAlphabet } from "nanoid";
-import { $transaction, prisma, PrismaClient } from "~/db.server";
-import { AuthenticatedEnvironment } from "../apiAuth.server";
-import { EndpointApi } from "../endpointApi.server";
-import { workerQueue } from "../worker.server";
-import { env } from "~/env.server";
-import { RuntimeEnvironmentType } from "~/database-types";
-
-const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
-
-export class CreateEndpointError extends Error {
- code: "FAILED_PING" | "FAILED_UPSERT";
- constructor(code: "FAILED_PING" | "FAILED_UPSERT", message: string) {
- super(message);
- Object.setPrototypeOf(this, CreateEndpointError.prototype);
- this.code = code;
- }
-}
-
-export class CreateEndpointService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call({
- environment,
- url,
- id,
- }: {
- environment: AuthenticatedEnvironment;
- url: string;
- id: string;
- }) {
- const endpointUrl = this.#normalizeEndpointUrl(url);
-
- const client = new EndpointApi(environment.apiKey, endpointUrl);
-
- const pong = await client.ping(id);
-
- if (!pong.ok) {
- throw new CreateEndpointError("FAILED_PING", pong.error);
- }
-
- try {
- const result = await $transaction(this.#prismaClient, async (tx) => {
- const endpoint = await tx.endpoint.upsert({
- where: {
- environmentId_slug: {
- environmentId: environment.id,
- slug: id,
- },
- },
- include: {
- environment: true,
- },
- create: {
- environment: {
- connect: {
- id: environment.id,
- },
- },
- organization: {
- connect: {
- id: environment.organizationId,
- },
- },
- project: {
- connect: {
- id: environment.projectId,
- },
- },
- slug: id,
- url: endpointUrl,
- indexingHookIdentifier: indexingHookIdentifier(),
- version: pong.triggerVersion,
- },
- update: {
- url: endpointUrl,
- version: pong.triggerVersion,
- },
- });
-
- const endpointIndex = await tx.endpointIndex.create({
- data: {
- endpointId: endpoint.id,
- status: "PENDING",
- source: "INTERNAL",
- },
- });
-
- // Kick off process to fetch the jobs for this endpoint
- await workerQueue.enqueue(
- "performEndpointIndexing",
- {
- id: endpointIndex.id,
- },
- {
- tx,
- maxAttempts:
- endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
- }
- );
-
- return { ...endpoint, endpointIndex };
- });
-
- return result;
- } catch (error) {
- if (error instanceof Error) {
- throw new CreateEndpointError("FAILED_UPSERT", error.message);
- } else {
- throw new CreateEndpointError("FAILED_UPSERT", "Something went wrong");
- }
- }
- }
-
- // If the endpoint URL points to localhost, and the RUNTIME_PLATFORM is docker-compose, then we need to rewrite the host to host.docker.internal
- // otherwise we shouldn't change anything
- #normalizeEndpointUrl(url: string) {
- if (env.RUNTIME_PLATFORM === "docker-compose") {
- const urlObj = new URL(url);
-
- if (urlObj.hostname === "localhost") {
- urlObj.hostname = "host.docker.internal";
- return urlObj.toString();
- }
- }
-
- return url;
- }
-}
diff --git a/apps/webapp/app/services/endpoints/deleteEndpointService.ts b/apps/webapp/app/services/endpoints/deleteEndpointService.ts
deleted file mode 100644
index 6c222a2193..0000000000
--- a/apps/webapp/app/services/endpoints/deleteEndpointService.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { PrismaClient } from "@trigger.dev/database";
-import { prisma } from "~/db.server";
-
-export class DeleteEndpointService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string, userId: string): Promise {
- await this.#prismaClient.endpoint.update({
- data: {
- url: null,
- },
- where: {
- id,
- organization: {
- members: {
- some: {
- userId,
- },
- },
- },
- },
- });
- }
-}
diff --git a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts
deleted file mode 100644
index 27df22c4fc..0000000000
--- a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { EndpointIndexSource } from "@trigger.dev/database";
-import { PrismaClient, prisma } from "~/db.server";
-import { PerformEndpointIndexService } from "./performEndpointIndexService";
-
-export class IndexEndpointService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(
- id: string,
- source: EndpointIndexSource = "INTERNAL",
- reason?: string,
- sourceData?: any
- ) {
- const endpointIndex = await this.#prismaClient.endpointIndex.create({
- data: {
- endpointId: id,
- status: "PENDING",
- source,
- reason,
- sourceData,
- },
- });
-
- const performEndpointIndexService = new PerformEndpointIndexService();
- return await performEndpointIndexService.call(endpointIndex.id);
- }
-}
diff --git a/apps/webapp/app/services/endpoints/performEndpointIndexService.ts b/apps/webapp/app/services/endpoints/performEndpointIndexService.ts
deleted file mode 100644
index db192ef4c2..0000000000
--- a/apps/webapp/app/services/endpoints/performEndpointIndexService.ts
+++ /dev/null
@@ -1,527 +0,0 @@
-import { PrismaClient, prisma } from "~/db.server";
-import { EndpointApi } from "../endpointApi.server";
-import { RegisterJobService } from "../jobs/registerJob.server";
-import { logger } from "../logger.server";
-import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server";
-import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
-import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
-import { DisableJobService } from "../jobs/disableJob.server";
-import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server";
-import { EndpointIndexError } from "@trigger.dev/core";
-import { safeBodyFromResponse } from "~/utils/json";
-import { fromZodError } from "zod-validation-error";
-import { IndexEndpointStats } from "@trigger.dev/core";
-import { RegisterHttpEndpointService } from "../triggers/registerHttpEndpoint.server";
-import { RegisterWebhookService } from "../triggers/registerWebhook.server";
-import { EndpointIndex } from "@trigger.dev/database";
-import { env } from "~/env.server";
-
-const MAX_SEQUENTIAL_FAILURE_COUNT = env.MAX_SEQUENTIAL_INDEX_FAILURE_COUNT;
-
-export class PerformEndpointIndexService {
- #prismaClient: PrismaClient;
- #registerJobService = new RegisterJobService();
- #disableJobService = new DisableJobService();
- #registerSourceServiceV1 = new RegisterSourceServiceV1();
- #registerSourceServiceV2 = new RegisterSourceServiceV2();
- #registerDynamicTriggerService = new RegisterDynamicTriggerService();
- #registerDynamicScheduleService = new RegisterDynamicScheduleService();
- #registerHttpEndpointService = new RegisterHttpEndpointService();
- #registerWebhookService = new RegisterWebhookService();
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string, redirectCount = 0): Promise {
- const endpointIndex = await this.#prismaClient.endpointIndex.update({
- where: {
- id,
- },
- data: {
- status: "STARTED",
- },
- include: {
- endpoint: {
- include: {
- environment: {
- include: {
- organization: true,
- project: true,
- },
- },
- },
- },
- },
- });
-
- logger.debug("Performing endpoint index", endpointIndex);
-
- if (!endpointIndex.endpoint.url) {
- logger.debug("Endpoint URL is not set", endpointIndex);
-
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: "Endpoint URL is not set",
- },
- false
- );
- }
-
- // Make a request to the endpoint to fetch a list of jobs
- const client = new EndpointApi(
- endpointIndex.endpoint.environment.apiKey,
- endpointIndex.endpoint.url
- );
- const { response, parser, headerParser, errorParser } = await client.indexEndpoint();
-
- if (!response) {
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: `Could not connect to endpoint ${endpointIndex.endpoint.url}`,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- if (isRedirect(response.status)) {
- // Update the endpoint URL with the response.headers.location
- logger.debug("Endpoint is redirecting", {
- headers: Object.fromEntries(response.headers.entries()),
- });
-
- const location = response.headers.get("location");
-
- if (!location) {
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: `Endpoint ${endpointIndex.endpoint.url} is redirecting but no location header is present`,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- if (redirectCount > 5) {
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: `Endpoint ${endpointIndex.endpoint.url} is redirecting too many times`,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- await this.#prismaClient.endpoint.update({
- where: {
- id: endpointIndex.endpoint.id,
- },
- data: {
- url: location,
- },
- });
-
- // Re-run the endpoint index
- return await this.call(id, redirectCount + 1);
- }
-
- if (response.status === 401) {
- const body = await safeBodyFromResponse(response, errorParser);
-
- if (body) {
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: body.message,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: "Trigger API key is invalid",
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- if (!response.ok) {
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: `Could not connect to endpoint ${endpointIndex.endpoint.url}. Status code: ${response.status}`,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- const anyBody = await response.json();
- const bodyResult = parser.safeParse(anyBody);
-
- if (!bodyResult.success) {
- const issues: string[] = [];
- bodyResult.error.issues.forEach((issue) => {
- if (issue.path.at(0) === "jobs") {
- const jobIndex = issue.path.at(1) as number;
- const job = (anyBody as any).jobs[jobIndex];
-
- if (job) {
- issues.push(`Job "${job.id}": ${issue.message} at "${issue.path.slice(2).join(".")}".`);
- }
- }
- });
-
- let friendlyError: string | undefined;
- if (issues.length > 0) {
- friendlyError = `Your Jobs have issues:\n${issues.map((issue) => `- ${issue}`).join("\n")}`;
- } else {
- friendlyError = fromZodError(bodyResult.error, {
- prefix: "There's an issue with the format of your Jobs",
- }).message;
- }
-
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: friendlyError,
- raw: fromZodError(bodyResult.error).message,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- const headerResult = headerParser.safeParse(Object.fromEntries(response.headers.entries()));
- if (!headerResult.success) {
- const friendlyError = fromZodError(headerResult.error, {
- prefix: "Your headers are invalid",
- });
- return updateEndpointIndexWithError(
- this.#prismaClient,
- id,
- endpointIndex.endpoint.id,
- {
- message: friendlyError.message,
- raw: headerResult.error.issues,
- },
- endpointIndex.endpoint.environment.type !== "DEVELOPMENT"
- );
- }
-
- const { jobs, sources, dynamicTriggers, dynamicSchedules, httpEndpoints, webhooks } =
- bodyResult.data;
- const { "trigger-version": triggerVersion, "trigger-sdk-version": triggerSdkVersion } =
- headerResult.data;
- const { endpoint } = endpointIndex;
-
- if (
- (triggerVersion && triggerVersion !== endpoint.version) ||
- (triggerSdkVersion && triggerSdkVersion !== endpoint.sdkVersion)
- ) {
- await this.#prismaClient.endpoint.update({
- where: {
- id: endpoint.id,
- },
- data: {
- version: triggerVersion,
- sdkVersion: triggerSdkVersion,
- },
- });
- }
-
- const indexStats: IndexEndpointStats = {
- jobs: 0,
- sources: 0,
- webhooks: 0,
- dynamicTriggers: 0,
- dynamicSchedules: 0,
- disabledJobs: 0,
- httpEndpoints: 0,
- };
-
- const existingJobs = await this.#prismaClient.job.findMany({
- where: {
- projectId: endpoint.projectId,
- deletedAt: null,
- },
- include: {
- aliases: {
- where: {
- name: "latest",
- environmentId: endpoint.environmentId,
- },
- include: {
- version: true,
- },
- take: 1,
- },
- },
- });
-
- for (const job of jobs) {
- if (!job.enabled) {
- const disabledJob = await this.#disableJobService
- .call(endpoint, { slug: job.id, version: job.version })
- .catch((error) => {
- logger.error("Failed to disable job", {
- endpointId: endpoint.id,
- job,
- error,
- });
-
- return;
- });
-
- if (disabledJob) {
- indexStats.disabledJobs++;
- }
- } else {
- try {
- const registeredVersion = await this.#registerJobService.call(endpoint, job);
-
- if (registeredVersion) {
- if (!job.internal) {
- indexStats.jobs++;
- }
- }
- } catch (error) {
- logger.error("Failed to register job", {
- endpointId: endpoint.id,
- job,
- error,
- });
- }
- }
- }
-
- // TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
- const missingJobs = existingJobs.filter((job) => {
- return !jobs.find((j) => j.id === job.slug);
- });
-
- if (missingJobs.length > 0) {
- logger.debug("Disabling missing jobs", {
- endpointId: endpoint.id,
- missingJobIds: missingJobs.map((job) => job.slug),
- });
-
- for (const job of missingJobs) {
- const latestVersion = job.aliases[0]?.version;
-
- if (!latestVersion) {
- continue;
- }
-
- const disabledJob = await this.#disableJobService
- .call(endpoint, {
- slug: job.slug,
- version: latestVersion.version,
- })
- .catch((error) => {
- logger.error("Failed to disable job", {
- endpointId: endpoint.id,
- job,
- error,
- });
-
- return;
- });
-
- if (disabledJob) {
- indexStats.disabledJobs++;
- }
- }
- }
-
- for (const source of sources) {
- try {
- switch (source.version) {
- default:
- case "1": {
- await this.#registerSourceServiceV1.call(endpoint, source);
- break;
- }
- case "2": {
- await this.#registerSourceServiceV2.call(endpoint, source);
- break;
- }
- }
-
- indexStats.sources++;
- } catch (error) {
- logger.error("Failed to register source", {
- endpointId: endpoint.id,
- source,
- error,
- });
- }
- }
-
- for (const dynamicTrigger of dynamicTriggers) {
- try {
- await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger);
-
- indexStats.dynamicTriggers++;
- } catch (error) {
- logger.error("Failed to register dynamic trigger", {
- endpointId: endpoint.id,
- dynamicTrigger,
- error,
- });
- }
- }
-
- for (const dynamicSchedule of dynamicSchedules) {
- try {
- await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule);
-
- indexStats.dynamicSchedules++;
- } catch (error) {
- logger.error("Failed to register dynamic schedule", {
- endpointId: endpoint.id,
- dynamicSchedule,
- error,
- });
- }
- }
-
- if (httpEndpoints) {
- for (const httpEndpoint of httpEndpoints) {
- try {
- await this.#registerHttpEndpointService.call(endpoint, httpEndpoint);
- indexStats.httpEndpoints++;
- } catch (error) {
- logger.error("Failed to register http endpoint", {
- endpointId: endpoint.id,
- httpEndpoint,
- error,
- });
- }
- }
- }
-
- if (webhooks) {
- for (const webhook of webhooks) {
- try {
- await this.#registerWebhookService.call(endpoint, webhook);
- indexStats.webhooks = indexStats.webhooks ?? 0 + 1;
- } catch (error) {
- logger.error("Failed to register webhook", {
- endpointId: endpoint.id,
- webhook,
- error,
- });
- }
- }
- }
-
- logger.debug("Endpoint indexing complete", {
- endpointId: endpoint.id,
- indexStats,
- source: endpointIndex.source,
- sourceData: endpointIndex.sourceData,
- reason: endpointIndex.reason,
- });
-
- return await this.#prismaClient.endpointIndex.update({
- where: {
- id,
- },
- data: {
- status: "SUCCESS",
- stats: indexStats,
- data: {
- jobs,
- sources,
- webhooks,
- dynamicTriggers,
- dynamicSchedules,
- httpEndpoints,
- },
- },
- });
- }
-}
-
-async function updateEndpointIndexWithError(
- prismaClient: PrismaClient,
- id: string,
- endpointId: string,
- error: EndpointIndexError,
- checkDisabling = true
-) {
- // Check here to see if this endpoint has only failed for the last 50 times
- // And if so, we disable the endpoint by setting the url to null
- if (checkDisabling) {
- const recentIndexes = await prismaClient.endpointIndex.findMany({
- where: {
- endpointId,
- id: {
- not: id,
- },
- },
- orderBy: {
- createdAt: "desc",
- },
- take: MAX_SEQUENTIAL_FAILURE_COUNT - 1,
- select: {
- status: true,
- },
- });
-
- if (
- recentIndexes.length === MAX_SEQUENTIAL_FAILURE_COUNT - 1 &&
- recentIndexes.every((index) => index.status === "FAILURE")
- ) {
- logger.debug("Disabling endpoint", {
- endpointId,
- error,
- });
-
- await prismaClient.endpoint.update({
- where: {
- id: endpointId,
- },
- data: {
- url: null,
- },
- });
- }
- }
-
- return await prismaClient.endpointIndex.update({
- where: {
- id,
- },
- data: {
- status: "FAILURE",
- error,
- },
- });
-}
-
-const redirectStatus = [301, 302, 303, 307, 308];
-const redirectStatusSet = new Set(redirectStatus);
-
-function isRedirect(status: number) {
- return redirectStatusSet.has(status);
-}
diff --git a/apps/webapp/app/services/endpoints/probeEndpoint.server.ts b/apps/webapp/app/services/endpoints/probeEndpoint.server.ts
deleted file mode 100644
index 40ef84fe81..0000000000
--- a/apps/webapp/app/services/endpoints/probeEndpoint.server.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { MAX_RUN_CHUNK_EXECUTION_LIMIT } from "~/consts";
-import { prisma, PrismaClient } from "~/db.server";
-import { EndpointApi } from "../endpointApi.server";
-import { logger } from "../logger.server";
-import { detectResponseIsTimeout } from "~/models/endpoint.server";
-
-export class ProbeEndpointService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string) {
- const endpoint = await this.#prismaClient.endpoint.findUnique({
- where: {
- id,
- },
- include: {
- environment: true,
- },
- });
-
- if (!endpoint) {
- return;
- }
-
- logger.debug(`Probing endpoint`, {
- id,
- });
-
- if (!endpoint.url) {
- logger.debug(`Endpoint has no url`, {
- id,
- });
- return;
- }
-
- const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url);
-
- const { response, durationInMs } = await client.probe(MAX_RUN_CHUNK_EXECUTION_LIMIT);
-
- if (!response) {
- return;
- }
-
- logger.debug(`Probing endpoint complete`, {
- id,
- durationInMs,
- response: {
- status: response.status,
- headers: Object.fromEntries(response.headers.entries()),
- },
- });
-
- const rawBody = await response.text();
-
- // If the response is a 200, or it was a timeout, we can assume the endpoint is up and update the runChunkExecutionLimit
- if (response.status === 200 || detectResponseIsTimeout(rawBody, response)) {
- await this.#prismaClient.endpoint.update({
- where: {
- id,
- },
- data: {
- runChunkExecutionLimit: Math.min(
- Math.max(durationInMs, 10000),
- MAX_RUN_CHUNK_EXECUTION_LIMIT
- ),
- },
- });
- }
- }
-}
diff --git a/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts b/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts
deleted file mode 100644
index 382a2331de..0000000000
--- a/apps/webapp/app/services/endpoints/recurringEndpointIndex.server.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { PrismaClient, prisma } from "~/db.server";
-import { logger } from "../logger.server";
-import { workerQueue } from "../worker.server";
-import { RuntimeEnvironmentType } from "@trigger.dev/database";
-
-export class RecurringEndpointIndexService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(ts: Date) {
- // Find all production endpoints that haven't been indexed in the last 10 minutes
- const currentTimestamp = ts.getTime();
-
- const endpoints = await this.#prismaClient.endpoint.findMany({
- where: {
- url: {
- not: null,
- },
- environment: {
- type: {
- in: [RuntimeEnvironmentType.PRODUCTION, RuntimeEnvironmentType.STAGING],
- },
- },
- indexings: {
- none: {
- createdAt: {
- gt: new Date(currentTimestamp - 60 * 60 * 1000),
- },
- },
- },
- },
- });
-
- logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", {
- count: endpoints.length,
- });
- // Enqueue each endpoint for indexing
- for (const endpoint of endpoints) {
- const index = await this.#prismaClient.endpointIndex.create({
- data: {
- endpointId: endpoint.id,
- status: "PENDING",
- source: "INTERNAL",
- },
- });
-
- await workerQueue.enqueue("performEndpointIndexing", {
- id: index.id,
- });
- }
- }
-}
diff --git a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts
deleted file mode 100644
index f5d2053121..0000000000
--- a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { customAlphabet } from "nanoid";
-import { $transaction, prisma, PrismaClient } from "~/db.server";
-import { env } from "~/env.server";
-import { AuthenticatedEnvironment } from "../apiAuth.server";
-import { workerQueue } from "../worker.server";
-import { CreateEndpointError } from "./createEndpoint.server";
-import { EndpointApi } from "../endpointApi.server";
-import { RuntimeEnvironmentType } from "~/database-types";
-
-const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
-
-export class ValidateCreateEndpointService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call({ environment, url }: { environment: AuthenticatedEnvironment; url: string }) {
- const endpointUrl = this.#normalizeEndpointUrl(url);
-
- const client = new EndpointApi(environment.apiKey, endpointUrl);
-
- const validationResult = await client.validate();
-
- if (!validationResult.ok) {
- throw new Error(validationResult.error);
- }
-
- try {
- const result = await $transaction(this.#prismaClient, async (tx) => {
- const endpoint = await tx.endpoint.upsert({
- where: {
- environmentId_slug: {
- environmentId: environment.id,
- slug: validationResult.endpointId,
- },
- },
- include: {
- environment: true,
- },
- create: {
- environment: {
- connect: {
- id: environment.id,
- },
- },
- organization: {
- connect: {
- id: environment.organizationId,
- },
- },
- project: {
- connect: {
- id: environment.projectId,
- },
- },
- slug: validationResult.endpointId,
- url: endpointUrl,
- indexingHookIdentifier: indexingHookIdentifier(),
- version: validationResult.triggerVersion,
- },
- update: {
- url: endpointUrl,
- version: validationResult.triggerVersion,
- },
- });
-
- const index = await tx.endpointIndex.create({
- data: { endpointId: endpoint.id, status: "PENDING", source: "INTERNAL" },
- });
-
- // Kick off process to fetch the jobs for this index
- await workerQueue.enqueue(
- "performEndpointIndexing",
- {
- id: index.id,
- },
- {
- tx,
- maxAttempts:
- endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
- }
- );
-
- return endpoint;
- });
-
- return result;
- } catch (error) {
- if (error instanceof Error) {
- throw new CreateEndpointError("FAILED_UPSERT", error.message);
- } else {
- throw new CreateEndpointError("FAILED_UPSERT", "Something went wrong");
- }
- }
- }
-
- // If the endpoint URL points to localhost, and the RUNTIME_PLATFORM is docker-compose, then we need to rewrite the host to host.docker.internal
- // otherwise we shouldn't change anything
- #normalizeEndpointUrl(url: string) {
- if (env.RUNTIME_PLATFORM === "docker-compose") {
- const urlObj = new URL(url);
-
- if (urlObj.hostname === "localhost") {
- urlObj.hostname = "host.docker.internal";
- return urlObj.toString();
- }
- }
-
- return url;
- }
-}
diff --git a/apps/webapp/app/services/events/cancelEvent.server.ts b/apps/webapp/app/services/events/cancelEvent.server.ts
deleted file mode 100644
index be91394764..0000000000
--- a/apps/webapp/app/services/events/cancelEvent.server.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { EventRecord } from "@trigger.dev/database";
-import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
-import { workerQueue } from "../worker.server";
-import { AuthenticatedEnvironment } from "../apiAuth.server";
-
-export class CancelEventService {
- #prismaClient: PrismaClientOrTransaction;
-
- constructor(prismaClient: PrismaClientOrTransaction = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(
- environment: AuthenticatedEnvironment,
- eventId: string
- ): Promise {
- return await $transaction(this.#prismaClient, async (tx) => {
- const event = await tx.eventRecord.findUnique({
- where: {
- eventId_environmentId: {
- eventId: eventId,
- environmentId: environment.id,
- },
- },
- });
-
- if (!event) {
- return;
- }
-
- if (event.cancelledAt) {
- return event;
- }
-
- //update the cancelledAt column in the eventRecord table
- const updatedEvent = await tx.eventRecord.update({
- where: { id: event.id },
- data: { cancelledAt: new Date() },
- });
-
- // Dequeue the event after the db has been updated
- await workerQueue.dequeue(`event:${event.id}`, { tx });
-
- return updatedEvent;
- });
- }
-}
diff --git a/apps/webapp/app/services/events/cancelRunsForEvent.server.ts b/apps/webapp/app/services/events/cancelRunsForEvent.server.ts
deleted file mode 100644
index a392c3e8f8..0000000000
--- a/apps/webapp/app/services/events/cancelRunsForEvent.server.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { PrismaClient } from "~/db.server";
-import { $transaction, prisma } from "~/db.server";
-import type { AuthenticatedEnvironment } from "../apiAuth.server";
-import { CancelRunService } from "../runs/cancelRun.server";
-import { logger } from "../logger.server";
-import type { CancelRunsForEvent } from "@trigger.dev/core";
-import type { JobRunStatus as JobRunStatusType } from "@trigger.dev/database";
-import { JobRunStatus } from "~/database-types";
-
-
-const CANCELLABLE_JOB_RUN_STATUS: Array = [
- JobRunStatus.PENDING,
- JobRunStatus.QUEUED,
- JobRunStatus.WAITING_ON_CONNECTIONS,
- JobRunStatus.PREPROCESSING,
- JobRunStatus.STARTED,
-];
-
-export class CancelRunsForEventService {
- #prismaClient: PrismaClient;
-
- constructor(prismaClient: PrismaClient = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(environment: AuthenticatedEnvironment, eventId: string) {
- return await $transaction(this.#prismaClient, async (tx) => {
- const event = await tx.eventRecord.findUnique({
- where: {
- eventId_environmentId: {
- eventId: eventId,
- environmentId: environment.id,
- },
- },
- });
-
- if (!event) {
- return;
- }
-
- const jobRuns = await tx.jobRun.findMany({
- where: {
- eventId: event.id,
- status: {
- in: CANCELLABLE_JOB_RUN_STATUS,
- },
- },
- select: {
- id: true,
- },
- });
-
- const cancelRunService = new CancelRunService(this.#prismaClient);
- const cancelledRunIds: string[] = [];
- const failedToCancelRunIds: string[] = [];
-
- for (const jobRun of jobRuns) {
- try {
- await cancelRunService.call({ runId: jobRun.id });
- cancelledRunIds.push(jobRun.id);
- } catch (err) {
- logger.debug(`failed to cancel job run with id ${jobRun.id} for event id ${eventId}`);
- failedToCancelRunIds.push(jobRun.id);
- }
- }
-
- return {
- cancelledRunIds: cancelledRunIds,
- failedToCancelRunIds: failedToCancelRunIds,
- };
- });
- }
-}
diff --git a/apps/webapp/app/services/events/deliverEvent.server.ts b/apps/webapp/app/services/events/deliverEvent.server.ts
deleted file mode 100644
index 9ab39c7ee1..0000000000
--- a/apps/webapp/app/services/events/deliverEvent.server.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-import { fromZodError } from "zod-validation-error";
-import type { EventDispatcher, EventRecord } from "@trigger.dev/database";
-import type { EventFilter } from "@trigger.dev/core";
-import { EventFilterSchema, RequestWithRawBodySchema, eventFilterMatches } from "@trigger.dev/core";
-import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
-import { logger } from "~/services/logger.server";
-import { workerQueue } from "../worker.server";
-
-class AlreadyDeliveredError extends Error {
- constructor() {
- super("Event already delivered");
- }
-}
-
-export class DeliverEventService {
- #prismaClient: PrismaClientOrTransaction;
-
- constructor(prismaClient: PrismaClientOrTransaction = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string) {
- try {
- await $transaction(
- this.#prismaClient,
- async (tx) => {
- const eventRecord = await tx.eventRecord.findUniqueOrThrow({
- where: {
- id,
- },
- include: {
- environment: {
- include: {
- organization: true,
- project: true,
- },
- },
- },
- });
-
- if (eventRecord.deliveredAt) {
- logger.debug("Event already delivered", {
- eventRecord: eventRecord.id,
- });
-
- return;
- }
-
- const possibleEventDispatchers = await tx.eventDispatcher.findMany({
- where: {
- environmentId: eventRecord.environmentId,
- event: {
- has: eventRecord.name,
- },
- source: eventRecord.source,
- enabled: true,
- manual: false,
- },
- });
-
- logger.debug("Found possible event dispatchers", {
- possibleEventDispatchers,
- eventRecord: eventRecord.id,
- });
-
- const matchingEventDispatchers = possibleEventDispatchers.filter((eventDispatcher) =>
- this.#evaluateEventRule(eventDispatcher, eventRecord)
- );
-
- if (matchingEventDispatchers.length === 0) {
- logger.debug("No matching event dispatchers", {
- eventRecord: eventRecord.id,
- });
-
- return;
- }
-
- logger.debug("Found matching event dispatchers", {
- matchingEventDispatchers,
- eventRecord: eventRecord.id,
- });
-
- await Promise.all(
- matchingEventDispatchers.map((eventDispatcher) =>
- workerQueue.enqueue(
- "events.invokeDispatcher",
- {
- id: eventDispatcher.id,
- eventRecordId: eventRecord.id,
- },
- { tx }
- )
- )
- );
-
- // Optimistically mark the event as delivered
- const lockedRecord = await tx.eventRecord.updateMany({
- where: {
- id: eventRecord.id,
- deliveredAt: null,
- },
- data: {
- deliveredAt: new Date(),
- },
- });
-
- if (lockedRecord.count === 0) {
- //this means we've already delivered it, because there were no records with deliveredAt = null
- //by throwing it will rollback the transaction, stopping the queue from processing the event again
- throw new AlreadyDeliveredError();
- }
- },
- { timeout: 10000 }
- );
- }
- catch (error) {
- if (error instanceof AlreadyDeliveredError) {
- logger.debug("Event already delivered, AlreadyDeliveredError", {
- eventRecord: id,
- });
-
- //we swallow the error because we don't want to retry
- return;
- }
-
- throw error;
- }
- }
-
- #evaluateEventRule(dispatcher: EventDispatcher, eventRecord: EventRecord): boolean {
- if (!dispatcher.payloadFilter && !dispatcher.contextFilter) {
- return true;
- }
-
- if (
- dispatcher.externalAccountId &&
- dispatcher.externalAccountId !== eventRecord.externalAccountId
- ) {
- return false;
- }
-
- const payloadFilter = EventFilterSchema.safeParse(dispatcher.payloadFilter ?? {});
-
- const contextFilter = EventFilterSchema.safeParse(dispatcher.contextFilter ?? {});
-
- if (!payloadFilter.success || !contextFilter.success) {
- logger.error("Invalid event filter", {
- payloadFilter,
- contextFilter,
- });
- return false;
- }
-
- const eventMatcher = new EventMatcher(eventRecord);
-
- return eventMatcher.matches({
- payload: payloadFilter.data,
- context: contextFilter.data,
- });
- }
-}
-
-export class EventMatcher {
- event: EventRecord;
-
- constructor(event: EventRecord) {
- this.event = event;
- }
-
- public matches(filter: EventFilter) {
- switch (this.event.payloadType) {
- case "REQUEST": {
- const result = RequestWithRawBodySchema.safeParse(this.event.payload);
-
- if (!result.success) {
- logger.error("Invalid payload, not a valid Raw REQUEST", {
- payload: this.event.payload,
- error: fromZodError(result.error).message,
- });
- return false;
- }
-
- const contentType = result.data.headers["content-type"];
-
- if (contentType?.includes("application/json")) {
- const body = JSON.parse(result.data.rawBody);
- const requestPayload = {
- url: result.data.url,
- method: result.data.method,
- headers: result.data.headers,
- body,
- };
-
- const isMatch = eventFilterMatches(
- {
- context: this.event.context,
- payload: requestPayload,
- },
- filter
- );
-
- return isMatch;
- }
-
- return eventFilterMatches(result.data, filter);
- }
- default: {
- return eventFilterMatches(this.event, filter);
- }
- }
- }
-}
diff --git a/apps/webapp/app/services/events/ingestSendEvent.server.ts b/apps/webapp/app/services/events/ingestSendEvent.server.ts
deleted file mode 100644
index 6efbbed611..0000000000
--- a/apps/webapp/app/services/events/ingestSendEvent.server.ts
+++ /dev/null
@@ -1,251 +0,0 @@
-import type { RawEvent, SendEventOptions } from "@trigger.dev/core";
-import { $transaction, PrismaClientOrTransaction, PrismaErrorSchema, prisma } from "~/db.server";
-import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
-import { workerQueue } from "~/services/worker.server";
-import { logger } from "../logger.server";
-import { EventRecord, ExternalAccount } from "@trigger.dev/database";
-import { Duration, RateLimiter } from "../rateLimiter.server";
-import { Ratelimit } from "@upstash/ratelimit";
-import { env } from "~/env.server";
-import { singleton } from "~/utils/singleton";
-
-type UpdateEventInput = {
- tx: PrismaClientOrTransaction;
- existingEventLog: EventRecord;
- reqEvent: RawEvent;
- deliverAt?: Date;
-};
-
-type CreateEventInput = {
- tx: PrismaClientOrTransaction;
- event: RawEvent;
- environment: AuthenticatedEnvironment;
- deliverAt?: Date;
- sourceContext?: { id: string; metadata?: any };
- externalAccount?: ExternalAccount;
- eventSource?: EventSource;
-};
-
-type EventSource = {
- httpEndpointId?: string;
- httpEndpointEnvironmentId?: string;
-};
-
-const EVENT_UPDATE_THRESHOLD_WINDOW_IN_MSECS = 5 * 1000; // 5 seconds
-
-const rateLimiter = singleton("eventRateLimiter", getSharedRateLimiter);
-
-function getSharedRateLimiter() {
- if (env.INGEST_EVENT_RATE_LIMIT_MAX) {
- return new RateLimiter({
- keyPrefix: "ingestsendevent",
- limiter: Ratelimit.slidingWindow(
- env.INGEST_EVENT_RATE_LIMIT_MAX,
- env.INGEST_EVENT_RATE_LIMIT_WINDOW as Duration
- ),
- });
- }
-}
-
-export class IngestSendEvent {
- #prismaClient: PrismaClientOrTransaction;
-
- constructor(prismaClient: PrismaClientOrTransaction = prisma, private deliverEvents = true) {
- this.#prismaClient = prismaClient;
- }
-
- #calculateDeliverAt(options?: SendEventOptions) {
- // If deliverAt is a string and a valid date, convert it to a Date object
- if (options?.deliverAt) {
- return options?.deliverAt;
- }
-
- // deliverAfter is the number of seconds to wait before delivering the event
- if (options?.deliverAfter) {
- return new Date(Date.now() + options.deliverAfter * 1000);
- }
-
- return undefined;
- }
-
- public async call(
- environment: AuthenticatedEnvironment,
- event: RawEvent,
- options?: SendEventOptions,
- sourceContext?: { id: string; metadata?: any },
- eventSource?: EventSource
- ) {
- try {
- const deliverAt = this.#calculateDeliverAt(options);
-
- if (!environment.organization.runsEnabled) {
- logger.debug("IngestSendEvent: Runs are disabled for this organization", environment);
- return;
- }
-
- const createdEvent = await $transaction(this.#prismaClient, async (tx) => {
- const externalAccount = options?.accountId
- ? await tx.externalAccount.upsert({
- where: {
- environmentId_identifier: {
- environmentId: environment.id,
- identifier: options.accountId,
- },
- },
- create: {
- environmentId: environment.id,
- organizationId: environment.organizationId,
- identifier: options.accountId,
- },
- update: {},
- })
- : undefined;
-
- const existingEventLog = await tx.eventRecord.findUnique({
- where: {
- eventId_environmentId: {
- eventId: event.id,
- environmentId: environment.id,
- },
- },
- });
-
- if (existingEventLog?.deliveredAt) {
- logger.debug("Event already delivered", {
- eventRecordId: existingEventLog.id,
- deliveredAt: existingEventLog.deliveredAt,
- });
- return existingEventLog;
- }
-
- const eventLog = await (existingEventLog
- ? this.updateEvent({ tx, existingEventLog, reqEvent: event, deliverAt })
- : this.createEvent({
- tx,
- event,
- environment,
- deliverAt,
- sourceContext,
- externalAccount,
- eventSource,
- }));
-
- return eventLog;
- });
-
- if (!createdEvent) return;
-
- if (createdEvent.deliveredAt) {
- logger.debug("Event already delivered", {
- eventRecordId: createdEvent.id,
- deliveredAt: createdEvent.deliveredAt,
- });
- //return the event if it was already delivered, don't enqueue it again
- return createdEvent;
- }
-
- //rate limit
- const result = await rateLimiter?.limit(environment.organizationId);
- if (result && !result.success) {
- logger.info("IngestSendEvent: Rate limit exceeded", {
- eventRecordId: createdEvent.id,
- organizationId: environment.organizationId,
- reset: result.reset,
- limit: result.limit,
- });
- return;
- }
-
- await this.enqueueWorkerEvent(this.#prismaClient, createdEvent);
- return createdEvent;
- } catch (error) {
- const prismaError = PrismaErrorSchema.safeParse(error);
-
- if (!prismaError.success) {
- logger.debug("Error parsing prisma error", {
- error,
- parseError: prismaError.error.format(),
- });
-
- throw error;
- }
-
- throw error;
- }
- }
-
- private async createEvent({
- tx,
- event,
- environment,
- deliverAt,
- sourceContext,
- externalAccount,
- eventSource,
- }: CreateEventInput) {
- const eventLog = await tx.eventRecord.create({
- data: {
- organizationId: environment.organizationId,
- projectId: environment.projectId,
- environmentId: environment.id,
- eventId: event.id,
- name: event.name,
- timestamp: event.timestamp ?? new Date(),
- payload: event.payload ?? {},
- payloadType: event.payloadType,
- context: event.context ?? {},
- source: event.source ?? "trigger.dev",
- sourceContext,
- deliverAt: deliverAt,
- externalAccountId: externalAccount ? externalAccount.id : undefined,
- httpEndpointId: eventSource?.httpEndpointId,
- httpEndpointEnvironmentId: eventSource?.httpEndpointEnvironmentId,
- },
- });
-
- return eventLog;
- }
-
- private async updateEvent({ tx, existingEventLog, reqEvent, deliverAt }: UpdateEventInput) {
- if (!this.shouldUpdateEvent(existingEventLog)) {
- logger.debug(`not updating event for event id: ${existingEventLog.eventId}`);
- return existingEventLog;
- }
-
- const updatedEventLog = await tx.eventRecord.update({
- where: {
- eventId_environmentId: {
- eventId: existingEventLog.eventId,
- environmentId: existingEventLog.environmentId,
- },
- },
- data: {
- payload: reqEvent.payload ?? existingEventLog.payload,
- payloadType: reqEvent.payloadType,
- context: reqEvent.context ?? existingEventLog.context,
- deliverAt: deliverAt ?? new Date(),
- },
- });
-
- return updatedEventLog;
- }
-
- private shouldUpdateEvent(eventLog: EventRecord) {
- const thresholdTime = new Date(Date.now() + EVENT_UPDATE_THRESHOLD_WINDOW_IN_MSECS);
-
- return eventLog.deliverAt >= thresholdTime;
- }
-
- private async enqueueWorkerEvent(tx: PrismaClientOrTransaction, eventLog: EventRecord) {
- if (this.deliverEvents) {
- // Produce a message to the event bus
- await workerQueue.enqueue(
- "deliverEvent",
- {
- id: eventLog.id,
- },
- { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
- );
- }
- }
-}
diff --git a/apps/webapp/app/services/events/invokeDispatcher.server.ts b/apps/webapp/app/services/events/invokeDispatcher.server.ts
deleted file mode 100644
index 1bd53be483..0000000000
--- a/apps/webapp/app/services/events/invokeDispatcher.server.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { z } from "zod";
-import type { PrismaClientOrTransaction } from "~/db.server";
-import { prisma } from "~/db.server";
-import { logger } from "~/services/logger.server";
-import { CreateRunService } from "~/services/runs/createRun.server";
-import { InvokeEphemeralDispatcherService } from "../dispatchers/invokeEphemeralEventDispatcher.server";
-import { DispatchableSchema } from "~/models/eventDispatcher.server";
-
-export class InvokeDispatcherService {
- #prismaClient: PrismaClientOrTransaction;
-
- constructor(prismaClient: PrismaClientOrTransaction = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string, eventRecordId: string) {
- const eventDispatcher = await this.#prismaClient.eventDispatcher.findUniqueOrThrow({
- where: {
- id,
- },
- include: {
- environment: {
- include: {
- project: true,
- organization: true,
- },
- },
- },
- });
-
- if (!eventDispatcher.enabled) {
- logger.debug("Event dispatcher is disabled", {
- eventDispatcher,
- });
-
- return;
- }
-
- const eventRecord = await this.#prismaClient.eventRecord.findUniqueOrThrow({
- where: {
- id: eventRecordId,
- },
- });
-
- logger.debug("Invoking event dispatcher", {
- eventDispatcher,
- eventRecord: eventRecord.id,
- });
-
- const dispatchable = DispatchableSchema.safeParse(eventDispatcher.dispatchable);
-
- if (!dispatchable.success) {
- logger.debug("Invalid dispatchable", {
- eventDispatcher,
- errors: dispatchable.error.flatten(),
- });
-
- return;
- }
-
- switch (dispatchable.data.type) {
- case "JOB_VERSION": {
- const jobVersion = await this.#prismaClient.jobVersion.findUniqueOrThrow({
- where: {
- id: dispatchable.data.id,
- },
- include: {
- job: true,
- },
- });
-
- const createRunService = new CreateRunService(this.#prismaClient);
-
- await createRunService.call({
- eventId: eventRecord.id,
- job: jobVersion.job,
- version: jobVersion,
- environment: eventDispatcher.environment,
- });
-
- break;
- }
- case "DYNAMIC_TRIGGER": {
- const dynamicTrigger = await this.#prismaClient.dynamicTrigger.findUniqueOrThrow({
- where: {
- id: dispatchable.data.id,
- },
- include: {
- endpoint: {
- include: {
- environment: {
- include: {
- project: true,
- organization: true,
- },
- },
- },
- },
- jobs: true,
- },
- });
-
- for (const job of dynamicTrigger.jobs) {
- const latestJobVersion = await this.#prismaClient.jobVersion.findFirst({
- where: {
- jobId: job.id,
- aliases: {
- some: {
- name: "latest",
- },
- },
- environmentId: dynamicTrigger.endpoint.environmentId,
- },
- orderBy: { createdAt: "desc" },
- take: 1,
- });
-
- if (!latestJobVersion) {
- continue;
- }
-
- const createRunService = new CreateRunService(this.#prismaClient);
-
- await createRunService.call({
- eventId: eventRecord.id,
- job: job,
- version: latestJobVersion,
- environment: eventDispatcher.environment,
- });
- }
-
- break;
- }
- case "EPHEMERAL": {
- await InvokeEphemeralDispatcherService.enqueue(eventDispatcher.id, eventRecord.id);
-
- break;
- }
- }
- }
-}
diff --git a/apps/webapp/app/services/events/sqsEventConsumer.ts b/apps/webapp/app/services/events/sqsEventConsumer.ts
deleted file mode 100644
index ceffee269a..0000000000
--- a/apps/webapp/app/services/events/sqsEventConsumer.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { Message, SQSClient } from "@aws-sdk/client-sqs";
-import { SendEventBodySchema } from "@trigger.dev/core";
-import { Consumer } from "sqs-consumer";
-import { z } from "zod";
-import { fromZodError } from "zod-validation-error";
-import { PrismaClientOrTransaction, prisma } from "~/db.server";
-import { env } from "~/env.server";
-import { authenticateApiKey } from "../apiAuth.server";
-import { logger, trace } from "../logger.server";
-import { IngestSendEvent } from "./ingestSendEvent.server";
-
-type SqsEventConsumerOptions = {
- queueUrl: string;
- /** This cannot be higher than the AWS limit of 10. */
- batchSize: number;
- region: string;
- accessKeyId: string;
- secretAccessKey: string;
- pollingWaitTimeMs: number;
-};
-
-const messageSchema = SendEventBodySchema.extend({
- apiKey: z.string(),
-});
-
-export class SqsEventConsumer {
- readonly #ingestEventService: IngestSendEvent;
- readonly #consumer: Consumer;
-
- constructor(
- readonly prismaClient: PrismaClientOrTransaction = prisma,
- options: SqsEventConsumerOptions
- ) {
- this.#ingestEventService = new IngestSendEvent();
-
- logger.debug("SqsEventConsumer starting", {
- queueUrl: options.queueUrl,
- region: options.region,
- });
-
- this.#consumer = Consumer.create({
- queueUrl: options.queueUrl,
- batchSize: options.batchSize,
- sqs: new SQSClient({
- region: options.region,
- credentials: {
- accessKeyId: options.accessKeyId,
- secretAccessKey: options.secretAccessKey,
- },
- }),
- handleMessage: async (message) => {
- await trace({ sqsMessage: message }, async () => await this.#processEvent(message));
- },
- });
-
- this.#consumer.on("error", (err, message) => {
- logger.error("SqsEventConsumer error", { error: err.message, sqsMessage: message });
- //todo what do we want to do here?
- });
-
- this.#consumer.on("processing_error", (err, message) => {
- logger.error("SqsEventConsumer processing_error", {
- error: err.message,
- sqsMessage: message,
- });
- //todo what do we want to do here?
- });
-
- this.#consumer.on("timeout_error", (err, message) => {
- logger.error("SqsEventConsumer timeout_error", { error: err.message, sqsMessage: message });
- //todo what do we want to do here?
- });
-
- //Stop the consumer if the process is terminated
- process.on("SIGTERM", () => {
- this.stop();
- });
-
- this.#consumer.start();
- }
-
- public stop() {
- logger.debug("SqsEventConsumer stopping");
- this.#consumer.stop({ abort: true });
- }
-
- async #processEvent(message: Message) {
- logger.debug("SqsEventConsumer processing event");
-
- //parse the body
- if (!message.Body) {
- logger.error("SqsEventConsumer message has no body");
- return;
- }
-
- const body = messageSchema.safeParse(JSON.parse(message.Body));
- if (!body.success) {
- logger.error("SqsEventConsumer message body is invalid", {
- error: fromZodError(body.error).message,
- });
- return;
- }
-
- //authenticate API Key
- const authenticationResult = await authenticateApiKey(body.data.apiKey);
- if (!authenticationResult) {
- logger.warn("SqsEventConsumer message has invalid API key");
- return;
- }
-
- const authenticatedEnv = authenticationResult.environment;
-
- logger.info("sqs_event", { event: body.data.event, options: body.data.options });
-
- const event = await this.#ingestEventService.call(
- authenticatedEnv,
- body.data.event,
- body.data.options
- );
-
- if (!event) {
- logger.error("SqsEventConsumer failed to create event");
- return;
- }
-
- logger.debug("SqsEventConsumer processed event", { event });
- }
-}
-
-export function getSharedSqsEventConsumer() {
- if (
- env.AWS_SQS_QUEUE_URL &&
- env.AWS_SQS_REGION &&
- env.AWS_SQS_ACCESS_KEY_ID &&
- env.AWS_SQS_SECRET_ACCESS_KEY
- ) {
- const consumer = new SqsEventConsumer(undefined, {
- queueUrl: env.AWS_SQS_QUEUE_URL,
- batchSize: env.AWS_SQS_BATCH_SIZE,
- pollingWaitTimeMs: env.AWS_SQS_WAIT_TIME_MS,
- region: env.AWS_SQS_REGION,
- accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
- secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
- });
-
- return consumer;
- }
-}
diff --git a/apps/webapp/app/services/executions/createExecutionEvent.server.ts b/apps/webapp/app/services/executions/createExecutionEvent.server.ts
deleted file mode 100644
index 971ac1088f..0000000000
--- a/apps/webapp/app/services/executions/createExecutionEvent.server.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { PrismaClientOrTransaction, prisma } from "~/db.server";
-import { logger } from "../logger.server";
-
-export type CreateExecutionEventInput = {
- organizationId: string;
- projectId: string;
- environmentId: string;
- jobId: string;
- runId: string;
- eventTime: Date;
- eventType: "start" | "finish";
- drift?: number;
- concurrencyLimitGroupId?: string | null;
-};
-
-export class CreateExecutionEventService {
- constructor(private prismaClient: PrismaClientOrTransaction = prisma) {}
-
- public async call(input: CreateExecutionEventInput) {
- await this.prismaClient.$executeRaw`
- INSERT INTO "triggerdotdev_events"."run_executions" (
- "organization_id",
- "project_id",
- "environment_id",
- "job_id",
- "run_id",
- "event_time",
- "event_type",
- "drift_amount_in_ms",
- "concurrency_limit_group_id"
- ) VALUES (
- ${input.organizationId},
- ${input.projectId},
- ${input.environmentId},
- ${input.jobId},
- ${input.runId},
- ${input.eventTime},
- ${input.eventType === "start" ? 1 : -1},
- ${input.drift},
- ${input.concurrencyLimitGroupId}
- )
- `;
- }
-}
-
-export async function createExecutionEvent(
- input: CreateExecutionEventInput,
- options?: { prismaClient?: PrismaClientOrTransaction }
-) {
- const service = new CreateExecutionEventService(options?.prismaClient);
-
- try {
- return await service.call(input);
- } catch (error) {
- logger.error("Error creating execution event", { error });
- }
-}
diff --git a/apps/webapp/app/services/externalApis/apis.server.ts b/apps/webapp/app/services/externalApis/apis.server.ts
deleted file mode 100644
index 3bdd382c27..0000000000
--- a/apps/webapp/app/services/externalApis/apis.server.ts
+++ /dev/null
@@ -1,1039 +0,0 @@
-export type Api = {
- identifier: string;
- name: string;
- examples?: ApiExample[];
-};
-
-export type ApiExample = {
- title: string;
- version: string;
- codeUrl: string;
- slug: string;
-};
-
-export const apisList = [
- {
- identifier: "airtable",
- name: "Airtable",
- examples: [
- {
- title: "Update Airtable when a new subscription is added to Stripe.",
- slug: "stripe-sub-update-airtable",
- version: "1.0.0",
-
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/stripeNewSubscriptionUpdateAirtable.ts",
- },
- {
- title: "Add a new record to Airtable when a Typeform response is submitted.",
- version: "1.0.0",
- slug: "new-airtable-record-from-typeform",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/typeformNewSubmissionUpdateAirtable.ts",
- },
- {
- title: "Update Airtable database when there is a sale in Stripe.",
- version: "1.0.0",
- slug: "update-airtable-when-stripe-account-updated",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/syncStripeWithAirtable.ts",
- },
- ],
- },
- {
- identifier: "algolia",
- name: "Algolia",
- },
- {
- identifier: "anthropic",
- name: "Anthropic",
- },
- // {
- // identifier: "appsmith",
- // name: "Appsmith",
- // },
- // {
- // identifier: "appwrite",
- // name: "Appwrite",
- // },
- {
- identifier: "asana",
- name: "Asana",
- examples: [
- {
- title: "A job that is triggered by a Asana webhook.",
- slug: "asana-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/asana-http-endpoint.ts",
- },
- {
- title: "Get user details from Asana",
- slug: "get-user-details",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/asana.ts",
- },
- ],
- },
- {
- identifier: "aws",
- name: "AWS",
- examples: [
- {
- title: "Trigger an AWS Lambda function with a defined payload and log the results.",
- slug: "get-user-details",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/aws.ts",
- },
- {
- title: "A job that is triggered by an AWS webhook.",
- slug: "aws-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/aws-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "brex",
- name: "Brex",
- examples: [
- {
- title: "A job that is triggered by a Brex webhook.",
- slug: "brex-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/brex-http-endpoint.ts",
- },
- {
- title: "Create a new title in a Brex account.",
- slug: "create-new-brex-title",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/brex.ts",
- },
- ],
- },
- {
- identifier: "caldotcom",
- name: "Cal.com",
- examples: [
- {
- title: "Send a Slack message when meetings are booked or cancelled.",
- slug: "cal-slack-meeting-alert",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/cal-http-endpoint.ts",
- },
- {
- title: "Find all Cal.com bookings for a user.",
- slug: "cal-find-bookings",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/cal.ts",
- },
- ],
- },
- {
- identifier: "clerk",
- name: "Clerk",
- examples: [
- {
- title: "A job that is triggered by a Clerk webhook.",
- slug: "clerk-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/clerk-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "clickup",
- name: "ClickUp",
- },
- {
- identifier: "coda",
- name: "Coda",
- },
- {
- identifier: "crowddotdev",
- name: "Crowd.dev",
- },
- {
- identifier: "deepl",
- name: "DeepL",
- examples: [
- {
- title: "Translate some text with DeepL.",
- slug: "translate-text-with-deepl",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/deepl.ts",
- },
- ],
- },
-
- {
- identifier: "digitalocean",
- name: "DigitalOcean",
- examples: [
- {
- title: "DigitalOcean create Uptime",
- slug: "digitalocean-create-uptime",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/digitalocean.ts",
- },
- ],
- },
- {
- identifier: "discord",
- name: "Discord",
- examples: [
- {
- title: "A job that is triggered by a Discord webhook.",
- slug: "discord-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/discord-http-endpoint.ts",
- },
- {
- title: "Create a Discord bot and send a message to a channel.",
- slug: "discord-bot-send-message",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/discord.ts",
- },
- {
- title: "A job that is triggered by a Discord webhook.",
- slug: "discord-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/discord-http-endpoint.ts",
- },
- ],
- },
- // {
- // identifier: "documenso",
- // name: "Documenso",
- // },
- // {
- // identifier: "dropbox",
- // name: "Dropbox",
- // },
- // {
- // identifier: "facebook",
- // name: "Facebook",
- // },
- // {
- // identifier: "fastify",
- // name: "Fastify",
- // },
- // {
- // identifier: "flickr",
- // name: "Flickr",
- // },
- {
- identifier: "github",
- name: "GitHub",
- examples: [
- {
- title: "Send a message to a Slack channel when a repo is starred.",
- slug: "github-star-to-slack",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubNewStarToSlack.ts",
- },
- {
- title: "Create a Linear issue when a pull request is opened on a GitHub repo.",
- slug: "linear-ticket-on-github-pr",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearCreateIssueOnPR.ts",
- },
- {
- title:
- "Send a reminder message to a Slack channel if a GitHub issue is left open for 24 hours.",
- slug: "github-issue-reminder",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubIssueReminder.ts",
- },
-
- {
- title: "Add a custom label to a GitHub issue.",
- slug: "github-custom-label",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubNewIssueOpened.ts",
- },
- ],
- },
- {
- identifier: "giphy",
- name: "Giphy",
- },
- {
- identifier: "gmail",
- name: "Gmail",
- examples: [
- {
- title: "Send an email using Gmail.",
- slug: "send-email-with-gmail",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/gmail.ts",
- },
- {
- title: "A job that is triggered by a Gmail webhook.",
- slug: "gmail-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/gmail-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "googlecalendar",
- name: "Google Calendar",
- examples: [
- {
- title: "Create a new Google Calendar event",
- slug: "create-google-calendar-event",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-calendar.ts",
- },
- ],
- },
- {
- identifier: "googledocs",
- name: "Google Docs",
- examples: [
- {
- title: "A job that is triggered by a Google Docs webhook.",
- slug: "google-docs-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/google-docs-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "googledrive",
- name: "Google Drive",
- examples: [
- {
- title: "A job that is triggered by a Google Drive webhook.",
- slug: "google-drive-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/google-drive-http-endpoint.ts",
- },
- {
- title: "Update a filename in Google Drive.",
- slug: "update-google-drive-filename",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-drive.ts",
- },
- ],
- },
- {
- identifier: "googlemaps",
- name: "Google Maps",
- examples: [
- {
- title: "Make a geocode request with Google Maps.",
- slug: "google-maps-geocode",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-maps.ts",
- },
- ],
- },
- {
- identifier: "googlesheets",
- name: "Google Sheets",
- examples: [
- {
- title: "A job that is triggered by a Google Sheets webhook.",
- slug: "google-sheets-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/google-sheets-http-endpoint.ts",
- },
- {
- title: "Insert data into a row in Google Sheets.",
- slug: "insert-data-into-google-sheets",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-sheets.ts",
- },
- ],
- },
- {
- identifier: "hubspot",
- name: "HubSpot",
- examples: [
- {
- title: "A job that is triggered by a HubSpot webhook.",
- slug: "hubspot-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/hubspot-http-endpoint.ts",
- },
- {
- title: "Create a contact in HubSpot.",
- slug: "create-contact-in-hubspot",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/hubspot.ts",
- },
- ],
- },
- {
- identifier: "huggingface",
- name: "Hugging Face",
- examples: [
- {
- title: "A job that is triggered by a Hugging Face webhook.",
- slug: "hugging-face-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/hugging-face-http-endpoint.ts",
- },
- {
- title: "Text classification with Hugging Face.",
- slug: "text-classification-with-hugging-face",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/hugging-face.ts",
- },
- ],
- },
- {
- identifier: "infisical",
- name: "Infisical",
- },
- {
- identifier: "instagram",
- name: "Instagram",
- examples: [
- {
- title: "A job that is triggered by a Instagram webhook.",
- slug: "instagram-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/instagram-http-endpoint.ts",
- },
- {
- title: "Post an image to Instagram",
- slug: "post-image-to-instagram",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/instagram.ts",
- },
- ],
- },
- // {
- // identifier: "instabug",
- // name: "Instabug",
- // },
- // {
- // identifier: "keep",
- // name: "Keep",
- // },
- {
- identifier: "lemonsqueezy",
- name: "Lemon Squeezy",
- examples: [
- {
- title: "Get store information from Lemon Squeezy.",
- slug: "get-store-information",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/lemon-squeezy.ts",
- },
- ],
- },
- // {
- // identifier: "linkedin",
- // name: "LinkedIn",
- // },
- {
- identifier: "linear",
- name: "Linear",
- examples: [
- {
- title: "Post Linear issues to Slack every weekday at 9am using Cron.",
- slug: "daily-linear-issues-slack-alert",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearIssuesDailySlackAlert.ts",
- },
- {
- title: "Create a Linear issue when a pull request is opened on a GitHub repo.",
- slug: "linear-ticket-on-pr",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearCreateIssueOnPR.ts",
- },
- {
- title: "Automatically comment and like any new Linear issues.",
- slug: "automatically-comment-and-like-linear-issues",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearNewIssueReply.ts",
- },
- ],
- },
- {
- identifier: "loops",
- name: "Loops",
- examples: [
- {
- title: "Create a new contact in Loops.",
- slug: "create-new-contact-in-loops",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/loops.ts",
- },
- ],
- },
- // {
- // identifier: "lotus",
- // name: "Lotus",
- // },
- {
- identifier: "mailchimp",
- name: "Mailchimp",
- },
- {
- identifier: "mailgun",
- name: "Mailgun",
- examples: [
- {
- title: "A job that is triggered by a Mailgun webhook.",
- slug: "mailgun-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/mailgun-http-endpoint.ts",
- },
- {
- title: "Send an email with Mailgun.",
- slug: "send-email-with-mailgun",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/mailgun.ts",
- },
- ],
- },
- {
- identifier: "microsoftazure",
- name: "Microsoft Azure",
- examples: [
- {
- title: "A job that is triggered by a Microsoft Azure webhook.",
- slug: "microsoft-azure-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/azure-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "monday",
- name: "Monday",
- },
- {
- identifier: "mux",
- name: "Mux",
- },
- {
- identifier: "notion",
- name: "Notion",
- examples: [
- {
- title: "Retrieve a Notion page by ID.",
- slug: "retrieve-notion-page",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/notion.ts",
- },
- ],
- },
- {
- identifier: "novu",
- name: "Novu",
- examples: [
- {
- title: "A job that is triggered by a Novu webhook.",
- slug: "novu-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/novu-http-endpoint.ts",
- },
- {
- title: "Create a new subscriber in Novu",
- slug: "create-new-subscriber-in-novu",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/novu.ts",
- },
- ],
- },
- {
- identifier: "openai",
- name: "OpenAI",
- examples: [
- {
- title: "Summarize GitHub commits using OpenAI and then post them to Slack.",
- slug: "openai-summarize-github-commits",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/summarizeGitHubCommits.ts",
- },
- {
- title: "Generate a random joke using OpenAI.",
- slug: "openai-generate-random-joke",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/openAITellMeAJoke.ts",
- },
- {
- title: "Generate an image from a prompt using OpenAI.",
- slug: "openai-generate-image",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/openAIGenerateImage.ts",
- },
- ],
- },
- {
- identifier: "pagerduty",
- name: "PagerDuty",
- examples: [
- {
- title: "A job that is triggered by a PagerDuty webhook.",
- slug: "pagerduty-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/pagerduty-http-endpoint.ts",
- },
- {
- title: "Install an addon in PagerDuty",
- slug: "pagerduty-install-addon",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/pagerduty.ts",
- },
- ],
- },
- {
- identifier: "plain",
- name: "Plain",
- examples: [
- {
- title: "Update or create customer information based on an identifier.",
- slug: "plain-update-customer-information",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/plainUpdateCustomer.ts",
- },
- ],
- },
- // {
- // identifier: "posthog",
- // name: "Posthog",
- // },
- {
- identifier: "raycast",
- name: "Raycast",
- },
- {
- identifier: "reddit",
- name: "Reddit",
- },
- {
- identifier: "replicate",
- name: "Replicate",
- examples: [
- {
- title: "Generate a cinematic image with Replicate.",
- slug: "generate-cinematic-image",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/replicateCinematicPrompt.ts",
- },
- ],
- },
- {
- identifier: "resend",
- name: "Resend",
- examples: [
- {
- title: "Send a drip email campaign over 30 days, triggered by an event.",
- slug: "resend-send-drip-campaign",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/resendDripCampaign.tsx",
- },
- {
- title: "Send an email built using React with Resend.",
- slug: "send-react-email",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/resendSendReactEmail.tsx",
- },
- {
- title: "Send a basic email with Resend.",
- slug: "resend-send-basic-email",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/resendSendBasicEmail.ts",
- },
- ],
- },
- {
- identifier: "salesforce",
- name: "Salesforce",
- examples: [
- {
- title: "A job that is triggered by a Salesforce webhook.",
- slug: "salesforce-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/salesforce-http-endpoint.ts",
- },
- {
- title: "Create a new contact in Salesforce.",
- slug: "salesforce-create-contact",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/salesforce.ts",
- },
- ],
- },
- {
- identifier: "segment",
- name: "Segment",
- examples: [
- {
- title: "A job that is triggered by a Segment webhook.",
- slug: "segment-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/segment-http-endpoint.ts",
- },
- {
- title: "Get source information from Segment.",
- slug: "segment-get-source-information",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/segment.ts",
- },
- ],
- },
- {
- identifier: "sendgrid",
- name: "SendGrid",
- examples: [
- {
- title: "Send an activity summary email to users at 4pm every Friday.",
- slug: "sendgrid-send-activity-summary",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/weeklyUserActivitySummary.ts",
- },
- {
- title: "SendGrid send basic email.",
- slug: "sendgrid-send-basic-email",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/sendGridSendBasicEmail.ts",
- },
- ],
- },
- {
- identifier: "shopify",
- name: "Shopify",
- examples: [
- {
- title: "Update a product variant price in Shopify.",
- slug: "shopify-update-product-variant-price",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/shopify.ts",
- },
- ],
- },
- {
- identifier: "slack",
- name: "Slack",
- examples: [
- {
- title: "Posts Linear issues to Slack every weekday at 9am using Cron.",
- slug: "slack-daily-linear-issues",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearIssuesDailySlackAlert.ts",
- },
- {
- title: "Summarize GitHub commits using OpenAI and then post them to Slack.",
- slug: "slack-openai-summarize-github-commits",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/summarizeGitHubCommits.ts",
- },
- {
- title: "Send an activity summary email, and post it to Slack at 4pm every Friday.",
- slug: "slack-sendgrid-send-activity-summary",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/weeklyUserActivitySummary.ts",
- },
- {
- title: "Send a message to a Slack channel when a GitHub repo is starred.",
- slug: "slack-post-github",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubNewStarToSlack.ts",
- },
- {
- title:
- "Send a reminder message to a Slack channel if a GitHub issue is left open for 24 hours.",
- slug: "slack-github-issue-reminder",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubIssueReminder.ts",
- },
- ],
- },
- {
- identifier: "snyk",
- name: "Snyk",
- examples: [
- {
- title: "A job that is triggered by a Snyk webhook.",
- slug: "snyk-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/snyk-http-endpoint.ts",
- },
- {
- title: "Get user details from Snyk.",
- slug: "snyk-get-user-details",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/snyk.ts",
- },
- ],
- },
- {
- identifier: "square",
- name: "Square",
- examples: [
- {
- title: "A job that is triggered by a Square webhook.",
- slug: "square-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/square-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "spotify",
- name: "Spotify",
- },
- {
- identifier: "stabilityai",
- name: "Stability AI",
- examples: [
- {
- title: "Generate an image with Stability AI.",
- slug: "stabilityai-generate-image",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/stability.ts",
- },
- ],
- },
- {
- identifier: "stripe",
- name: "Stripe",
- examples: [
- {
- title: "Update Supabase every time a Stripe account is updated.",
- slug: "stripe-supabase-update",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/supabaseStripeUpdateDatabase.ts",
- },
- {
- title: "Update Airtable when a new subscription is added to Stripe.",
- slug: "stripe-sub-update-airtable",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/stripeNewSubscriptionUpdateAirtable.ts",
- },
- {
- title: "Update Airtable database when there is a sale in Stripe.",
- version: "1.0.0",
- slug: "update-airtable-when-stripe-account-updated",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/syncStripeWithAirtable.ts",
- },
- ],
- },
- {
- identifier: "supabase",
- name: "Supabase",
- examples: [
- {
- title: "Update Supabase every time a Stripe account is updated.",
- slug: "stripe-supabase-update",
- version: "1.0.0",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/supabaseStripeUpdateDatabase.ts",
- },
- ],
- },
- {
- identifier: "svix",
- name: "Svix",
- examples: [
- {
- title: "A job that is triggered by a Svix webhook.",
- slug: "svix-http-endpoint",
- version: "1.0.0",
- exampleType: ["http-endpoint"],
- apisUsed: ["svix"],
- tags: ["dev-ops"],
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/svix-http-endpoint.ts",
- },
- {
- title: "Create an application in Svix",
- slug: "svix-create-application",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/svix.ts",
- },
- ],
- },
- {
- identifier: "todoist",
- name: "Todoist",
- examples: [
- {
- title: "A job that is triggered by a Todoist webhook.",
- slug: "todoist-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/todoist-http-endpoint.ts",
- },
- {
- title: "Add a new project in Todoist.",
- slug: "todoist-add-new-project",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/todoist.ts",
- },
- ],
- },
- {
- identifier: "trello",
- name: "Trello",
- },
-
- {
- identifier: "twilio",
- name: "Twilio",
- examples: [
- {
- title: "Send an SMS or WhatsApp message with Twilio",
- slug: "twilio-send-sms-or-whatsapp-message",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/twilio.ts",
- },
- {
- title: "A job that is triggered by a Twilio webhook.",
- slug: "twilio-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/twilio-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "typeform",
- name: "Typeform",
- examples: [
- {
- title: "Add a new record to Airtable when a Typeform response is submitted.",
- version: "1.0.0",
- slug: "new-airtable-record-from-typeform",
- codeUrl:
- "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/typeformNewSubmissionUpdateAirtable.ts",
- },
- ],
- },
-
- {
- identifier: "whatsapp",
- name: "WhatsApp",
- examples: [
- {
- title: "A job that is triggered by a WhatsApp webhook.",
- slug: "whatsapp-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/whatsapp-http-endpoint.ts",
- },
- {
- title: "Send a message to a WhatsApp number",
- slug: "whatapp-send-message",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/whatsapp.ts",
- },
- ],
- },
- {
- identifier: "x",
- name: "X (Twitter)",
- examples: [
- {
- title: "Post a post to an X (Twitter) account",
- slug: "post-to-x",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/x.ts",
- },
- ],
- },
- {
- identifier: "youtube",
- name: "YouTube",
- examples: [
- {
- title: "A job that is triggered by a YouTube webhook.",
- slug: "youtube-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/youtube-http-endpoint.ts",
- },
- {
- title: "Search for a YouTube video",
- slug: "youtube-search-video",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/youtube.ts",
- },
- ],
- },
- {
- identifier: "zapier",
- name: "Zapier",
- examples: [
- {
- title: "Store name in Zapier",
- slug: "zapier-store-name",
- version: "1.0.0",
- codeUrl: "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/zapier.ts",
- },
- {
- title: "A job that is triggered by a Zapier webhook.",
- slug: "zapier-http-endpoint",
- version: "1.0.0",
- codeUrl:
- "https://raw.githubusercontent.com/triggerdotdev/api-reference/main/src/zapier-http-endpoint.ts",
- },
- ],
- },
- {
- identifier: "zbd",
- name: "ZBD",
- examples: [
- {
- title: "Send Satoshis to a ZBD account.",
- slug: "zbd-send-satoshis",
- version: "1.0.0",
- codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/zbd.ts",
- },
- ],
- },
-];
diff --git a/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts b/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts
deleted file mode 100644
index 23e4c77ee3..0000000000
--- a/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts
+++ /dev/null
@@ -1,852 +0,0 @@
-import type {
- ConnectionAttempt,
- ConnectionType,
- ExternalAccount,
- Integration,
- IntegrationAuthMethod,
- IntegrationConnection,
- IntegrationDefinition,
- SecretReference,
-} from "@trigger.dev/database";
-import jsonpointer from "jsonpointer";
-import { customAlphabet } from "nanoid";
-import * as crypto from "node:crypto";
-import type { PrismaClient, PrismaClientOrTransaction, PrismaTransactionClient } from "~/db.server";
-import { prisma } from "~/db.server";
-import { env } from "~/env.server";
-import { workerQueue } from "~/services/worker.server";
-import { getSecretStore } from "../secrets/secretStore.server";
-import type { IntegrationCatalog } from "./integrationCatalog.server";
-import { integrationCatalog } from "./integrationCatalog.server";
-import {
- createOAuth2Url,
- getClientConfig,
- grantOAuth2Token,
- refreshOAuth2Token,
-} from "./oauth2.server";
-import {
- AccessToken,
- AccessTokenSchema,
- ApiAuthenticationMethodOAuth2,
- ConnectionMetadata,
- GrantTokenParams,
- OAuthClient,
- OAuthClientSchema,
- RefreshTokenParams,
-} from "./types";
-import { logger } from "../logger.server";
-
-export type ConnectionWithSecretReference = IntegrationConnection & {
- dataReference: SecretReference;
-};
-
-/** How many seconds before expiry we should refresh the token */
-const tokenRefreshThreshold = 5 * 60;
-
-export class IntegrationAuthRepository {
- #integrationCatalog: IntegrationCatalog;
- #prismaClient: PrismaClient;
-
- constructor(
- catalog: IntegrationCatalog = integrationCatalog,
- prismaClient: PrismaClient = prisma
- ) {
- this.#integrationCatalog = catalog;
- this.#prismaClient = prismaClient;
- }
-
- /** Get all API connections for the organization, for a specific API */
- async getClientsForIntegration(organizationId: string, identifier: string) {
- const clients = await this.#prismaClient.integration.findMany({
- where: {
- organizationId: organizationId,
- definitionId: identifier,
- },
- include: {
- authMethod: true,
- definition: true,
- },
- });
-
- return clients.map((c) => this.#enrichIntegration(c, c.definition, c.authMethod));
- }
-
- async createConnectionClient({
- id,
- slug,
- customClient,
- organizationId,
- integrationIdentifier,
- integrationAuthMethod,
- clientType,
- scopes,
- title,
- description,
- url,
- redirectTo,
- }: {
- id: string;
- slug: string;
- customClient?: OAuthClient;
- organizationId: string;
- integrationIdentifier: string;
- integrationAuthMethod: string;
- clientType: ConnectionType;
- scopes: string[];
- title: string;
- description?: string;
- redirectTo: string;
- url: URL;
- }): Promise {
- return this.#prismaClient.$transaction(async (tx) => {
- let customClientReference: SecretReference | undefined = undefined;
- //if there's a custom client, we need to save the details to the secret store
- if (customClient) {
- const key = `connectionClient/customClient/${id}`;
-
- const secretStore = getSecretStore(env.SECRET_STORE, {
- prismaClient: tx,
- });
-
- await secretStore.setSecret(key, { ...customClient });
-
- customClientReference = await tx.secretReference.create({
- data: {
- key,
- provider: env.SECRET_STORE,
- },
- });
- }
-
- logger.debug("Creating Integration", {
- id,
- clientType,
- scopes,
- title,
- slug,
- integrationIdentifier,
- integrationAuthMethod,
- });
-
- const client = await tx.integration.create({
- data: {
- id,
- connectionType: clientType,
- scopes,
- title,
- slug,
- authSource: "HOSTED",
- description,
- customClientReference: customClientReference
- ? {
- connect: {
- id: customClientReference.id,
- },
- }
- : undefined,
- organization: {
- connect: {
- id: organizationId,
- },
- },
- authMethod: {
- connect: {
- definitionId_key: {
- definitionId: integrationIdentifier,
- key: integrationAuthMethod,
- },
- },
- },
- definition: {
- connect: {
- id: integrationIdentifier,
- },
- },
- },
- });
-
- return await this.createConnectionAttempt({
- tx,
- integration: client,
- customOAuthClient: customClient,
- redirectTo,
- url,
- });
- });
- }
-
- async populateMissingConnectionClientFields({
- id,
- customClient,
- organizationId,
- integrationIdentifier,
- integrationAuthMethod,
- clientType,
- scopes,
- title,
- description,
- url,
- redirectTo,
- }: {
- id: string;
- customClient?: OAuthClient;
- organizationId: string;
- integrationIdentifier: string;
- integrationAuthMethod: string;
- clientType: ConnectionType;
- scopes: string[];
- title: string;
- description?: string;
- redirectTo: string;
- url: URL;
- }): Promise {
- return this.#prismaClient.$transaction(async (tx) => {
- let customClientReference: SecretReference | undefined = undefined;
- //if there's a custom client, we need to save the details to the secret store
- if (customClient) {
- const key = `connectionClient/customClient/${id}`;
-
- const secretStore = getSecretStore(env.SECRET_STORE, {
- prismaClient: tx,
- });
-
- await secretStore.setSecret(key, { ...customClient });
-
- customClientReference = await tx.secretReference.create({
- data: {
- key,
- provider: env.SECRET_STORE,
- },
- });
- }
-
- const client = await tx.integration.update({
- where: {
- id,
- },
- data: {
- connectionType: clientType,
- scopes,
- title,
- description,
- setupStatus: "COMPLETE",
- customClientReference: customClientReference
- ? {
- connect: {
- id: customClientReference.id,
- },
- }
- : undefined,
- organization: {
- connect: {
- id: organizationId,
- },
- },
- authMethod: {
- connect: {
- definitionId_key: {
- definitionId: integrationIdentifier,
- key: integrationAuthMethod,
- },
- },
- },
- definition: {
- connect: {
- id: integrationIdentifier,
- },
- },
- },
- });
-
- return await this.createConnectionAttempt({
- tx,
- integration: client,
- customOAuthClient: customClient,
- redirectTo,
- url,
- });
- });
- }
-
- async createConnectionAttempt({
- tx,
- integration,
- customOAuthClient,
- redirectTo,
- url,
- }: {
- tx: PrismaTransactionClient;
- integration: Integration;
- customOAuthClient?: OAuthClient;
- redirectTo: string;
- url: URL;
- }) {
- const { authMethod } = await this.#getDefinitionAndAuthMethod(integration);
-
- switch (authMethod.type) {
- case "oauth2": {
- let pkceCode: string | undefined = undefined;
- if (authMethod.config.pkce !== false) {
- pkceCode = crypto.randomBytes(24).toString("hex");
- }
-
- //create a connection attempt
- const connectionAttempt = await tx.connectionAttempt.create({
- data: {
- integrationId: integration.id,
- redirectTo,
- securityCode: pkceCode,
- },
- });
-
- //get the client config, custom client or from env vars
- const clientConfig = getClientConfig({
- env: {
- idName: authMethod.client.id.envName,
- secretName: authMethod.client.secret.envName,
- },
- customOAuthClient,
- });
- const callbackUrl = this.#buildCallbackUrl({
- authenticationMethod: authMethod as ApiAuthenticationMethodOAuth2,
- url,
- hasCustomClient: !!customOAuthClient,
- clientId: integration.id,
- });
-
- const createAuthorizationParams = {
- authorizationUrl: authMethod.config.authorization.url,
- clientId: clientConfig.id,
- clientSecret: clientConfig.secret,
- key: connectionAttempt.id,
- callbackUrl,
- scopeParamName: authMethod.config.authorization.scopeParamName ?? "scope",
- scopes: integration.scopes,
- scopeSeparator: authMethod.config.authorization.scopeSeparator,
- pkceCode,
- authorizationLocation: authMethod.config.authorization.authorizationLocation ?? "body",
- extraParameters: authMethod.config.authorization.extraParameters,
- };
-
- const authorizationUrl = await createOAuth2Url(
- createAuthorizationParams,
- authMethod.config.authorization.createUrlStrategy
- );
-
- return authorizationUrl;
- }
- default: {
- throw new Error(`Authentication method type ${authMethod.type} not supported`);
- }
- }
- }
-
- async createConnectionFromAttempt({
- attempt,
- code,
- url,
- customOAuthClient,
- }: {
- attempt: ConnectionAttempt & { integration: Integration };
- code: string;
- url: URL;
- customOAuthClient?: OAuthClient;
- }) {
- const { definition, authMethod } = await this.#getDefinitionAndAuthMethod(attempt.integration);
-
- switch (authMethod.type) {
- case "oauth2": {
- const clientConfig = getClientConfig({
- env: {
- idName: authMethod.client.id.envName,
- secretName: authMethod.client.secret.envName,
- },
- customOAuthClient,
- });
- const callbackUrl = this.#buildCallbackUrl({
- authenticationMethod: authMethod as ApiAuthenticationMethodOAuth2,
- url,
- hasCustomClient: !!customOAuthClient,
- clientId: attempt.integration.id,
- });
-
- const params: GrantTokenParams = {
- tokenUrl: authMethod.config.token.url,
- clientId: clientConfig.id,
- clientSecret: clientConfig.secret,
- code,
- callbackUrl,
- requestedScopes: attempt.integration.scopes,
- scopeSeparator: authMethod.config.authorization.scopeSeparator,
- pkceCode: attempt.securityCode ?? undefined,
- accessTokenPointer: authMethod.config.token.accessTokenPointer ?? "/access_token",
- refreshTokenPointer: authMethod.config.token.refreshTokenPointer ?? "/refresh_token",
- expiresInPointer: authMethod.config.token.expiresInPointer ?? "/expires_in",
- scopePointer: authMethod.config.token.scopePointer ?? "/scope",
- authorizationMethod: authMethod.config.token.authorizationMethod,
- bodyFormat: authMethod.config.token.bodyFormat,
- };
-
- const token = await grantOAuth2Token(params, authMethod.config.token.grantTokenStrategy);
-
- //this key is used to store in the relevant SecretStore
- const hashedAccessToken = crypto
- .createHash("sha256")
- .update(token.accessToken)
- .digest("base64");
-
- const key = secretStoreKeyForToken(definition.identifier, hashedAccessToken);
-
- const metadata = this.#getMetadataFromToken({
- token,
- authenticationMethod: authMethod as ApiAuthenticationMethodOAuth2,
- });
-
- return await this.#prismaClient.$transaction(async (tx) => {
- let secretReference = await tx.secretReference.findUnique({
- where: {
- key,
- },
- });
-
- if (secretReference) {
- //if the secret reference already exists, update existing connections with the new scopes information
- await tx.integrationConnection.updateMany({
- where: {
- dataReferenceId: secretReference.id,
- },
- data: {
- scopes: token.scopes,
- metadata,
- },
- });
- } else {
- secretReference = await tx.secretReference.create({
- data: {
- key,
- provider: env.SECRET_STORE,
- },
- });
- }
-
- const secretStore = getSecretStore(env.SECRET_STORE, {
- prismaClient: tx,
- });
-
- await secretStore.setSecret(key, token);
-
- //if there's an expiry, we want to add it to the connection so we can easily run a background job against it
- const expiresAt = this.#getExpiresAtFromToken({ token });
-
- const connection = await tx.integrationConnection.create({
- data: {
- organizationId: attempt.integration.organizationId,
- integrationId: attempt.integration.id,
- metadata,
- dataReferenceId: secretReference.id,
- scopes: token.scopes,
- expiresAt,
- },
- });
-
- await workerQueue.enqueue(
- "connectionCreated",
- {
- id: connection.id,
- },
- { tx }
- );
-
- //schedule refreshing the token
- await this.#scheduleRefresh(expiresAt, connection, tx);
-
- return connection;
- });
- }
- }
- }
-
- async createConnectionFromToken({
- token,
- integration,
- externalAccount,
- }: {
- token: AccessToken;
- integration: Integration;
- externalAccount?: ExternalAccount;
- }) {
- const { definition, authMethod } = await this.#getDefinitionAndAuthMethod(integration);
-
- switch (authMethod.type) {
- case "oauth2": {
- //this key is used to store in the relevant SecretStore
- const hashedAccessToken = crypto
- .createHash("sha256")
- .update(token.accessToken)
- .digest("base64");
-
- const key = secretStoreKeyForToken(definition.identifier, hashedAccessToken);
-
- const metadata = this.#getMetadataFromToken({
- token,
- authenticationMethod: authMethod as ApiAuthenticationMethodOAuth2,
- });
-
- return await this.#prismaClient.$transaction(async (tx) => {
- let secretReference = await tx.secretReference.findUnique({
- where: {
- key,
- },
- });
-
- if (secretReference) {
- //if the secret reference already exists, update existing connections with the new scopes information
- await tx.integrationConnection.updateMany({
- where: {
- dataReferenceId: secretReference.id,
- },
- data: {
- scopes: token.scopes,
- metadata,
- },
- });
- } else {
- secretReference = await tx.secretReference.create({
- data: {
- key,
- provider: env.SECRET_STORE,
- },
- });
- }
-
- const secretStore = getSecretStore(env.SECRET_STORE, {
- prismaClient: tx,
- });
-
- await secretStore.setSecret(key, token);
-
- //if there's an expiry, we want to add it to the connection so we can easily run a background job against it
- const expiresAt = this.#getExpiresAtFromToken({ token });
-
- const connection = await tx.integrationConnection.create({
- data: {
- organizationId: integration.organizationId,
- integrationId: integration.id,
- metadata,
- dataReferenceId: secretReference.id,
- scopes: token.scopes,
- expiresAt,
- externalAccountId: externalAccount?.id,
- connectionType: externalAccount ? "EXTERNAL" : "DEVELOPER",
- },
- });
-
- await workerQueue.enqueue(
- "connectionCreated",
- {
- id: connection.id,
- },
- { tx }
- );
-
- //schedule refreshing the token
- await this.#scheduleRefresh(expiresAt, connection, tx);
-
- return connection;
- });
- }
- }
- }
-
- async refreshConnection({ connectionId }: { connectionId: string }) {
- const connection = await this.#prismaClient.integrationConnection.findUnique({
- where: {
- id: connectionId,
- },
- include: {
- dataReference: true,
- integration: {
- include: {
- customClientReference: true,
- },
- },
- },
- });
-
- if (!connection) {
- throw new Error(`Connection ${connectionId} not found`);
- }
-
- if (!connection.enabled) {
- logger.info("Connection is disabled", {
- connection,
- });
- return;
- }
-
- let customOAuthClient: OAuthClient | undefined;
- if (connection.integration.customClientReference) {
- const secretStore = getSecretStore(env.SECRET_STORE);
- customOAuthClient = await secretStore.getSecret(
- OAuthClientSchema,
- connection.integration.customClientReference.key
- );
- }
-
- const { authMethod } = await this.#getDefinitionAndAuthMethod(connection.integration);
-
- switch (authMethod.type) {
- case "oauth2": {
- const clientConfig = getClientConfig({
- env: {
- idName: authMethod.client.id.envName,
- secretName: authMethod.client.secret.envName,
- },
- customOAuthClient,
- });
-
- const secretStore = getSecretStore(connection.dataReference.provider);
- const accessToken = await secretStore.getSecret(
- AccessTokenSchema,
- connection.dataReference.key
- );
-
- if (!accessToken) {
- throw new Error(
- `Access token not found for connection ${connectionId} with key ${connection.dataReference.key}`
- );
- }
-
- if (!accessToken.refreshToken) {
- throw new Error(
- `Refresh token not found for connection ${connectionId} with key ${connection.dataReference.key}`
- );
- }
-
- if (!accessToken.expiresIn) {
- throw new Error(
- `Expires in not found for connection ${connectionId} with key ${connection.dataReference.key}`
- );
- }
-
- const params: RefreshTokenParams = {
- refreshUrl: authMethod.config.refresh.url,
- clientId: clientConfig.id,
- clientSecret: clientConfig.secret,
- requestedScopes: connection.integration.scopes,
- scopeSeparator: authMethod.config.authorization.scopeSeparator,
- token: {
- accessToken: accessToken.accessToken,
- refreshToken: accessToken.refreshToken,
- expiresAt: new Date(connection.updatedAt.getTime() + accessToken.expiresIn * 1000),
- },
- accessTokenPointer: authMethod.config.token.accessTokenPointer ?? "/access_token",
- refreshTokenPointer: authMethod.config.token.refreshTokenPointer ?? "/refresh_token",
- expiresInPointer: authMethod.config.token.expiresInPointer ?? "/expires_in",
- scopePointer: authMethod.config.token.scopePointer ?? "/scope",
- authorizationMethod: authMethod.config.token.authorizationMethod,
- bodyFormat: authMethod.config.token.bodyFormat,
- skipScopes: authMethod.config.refresh.skipScopes,
- };
-
- //todo do we need pkce here?
- const token = await refreshOAuth2Token(
- params,
- authMethod.config.refresh.refreshTokenStrategy
- );
-
- //update the secret
- await secretStore.setSecret(connection.dataReference.key, token);
-
- //update the connection
- const metadata = this.#getMetadataFromToken({
- token,
- authenticationMethod: authMethod as ApiAuthenticationMethodOAuth2,
- });
-
- const expiresAt = this.#getExpiresAtFromToken({ token });
- const newConnection = await this.#prismaClient.integrationConnection.update({
- where: {
- id: connectionId,
- },
- data: {
- metadata,
- scopes: token.scopes,
- expiresAt,
- },
- include: {
- dataReference: true,
- },
- });
-
- await this.#scheduleRefresh(expiresAt, connection);
- return newConnection;
- }
- default: {
- throw new Error(`Authentication method type ${authMethod.type} not supported`);
- }
- }
- }
-
- /** Get credentials for the ApiConnection */
- async getCredentials(connection: ConnectionWithSecretReference) {
- //refresh the token if the expiry is in the past (or about to be)
- if (connection.expiresAt) {
- const refreshBy = new Date(connection.expiresAt.getTime() - tokenRefreshThreshold * 1000);
- if (refreshBy < new Date()) {
- const refreshedConnection = await this.refreshConnection({
- connectionId: connection.id,
- });
-
- if (!refreshedConnection) {
- return;
- }
-
- connection = refreshedConnection;
- }
- }
-
- const secretStore = getSecretStore(connection.dataReference.provider);
- return secretStore.getSecret(AccessTokenSchema, connection.dataReference.key);
- }
-
- #enrichIntegration(
- integration: Integration,
- definition: IntegrationDefinition,
- authMethod?: IntegrationAuthMethod | null
- ) {
- if (!authMethod) {
- throw new Error(
- `Auth method ${integration.authMethodId} not found for integration ${definition.id}`
- );
- }
-
- if (authMethod.type !== "oauth2") {
- throw new Error(`Authentication method type ${authMethod.type} not supported`);
- }
-
- return {
- ...integration,
- definition: {
- identifier: definition.id,
- name: definition.name,
- },
- authMethod: {
- type: authMethod.type,
- name: authMethod.name,
- possibleScopes: authMethod.scopes,
- },
- };
- }
-
- async #getDefinitionAndAuthMethod(integration: Integration) {
- const definition = await this.#prismaClient.integrationDefinition.findUniqueOrThrow({
- where: {
- id: integration.definitionId,
- },
- });
-
- const authMethod = integration.authMethodId
- ? await this.#prismaClient.integrationAuthMethod.findUniqueOrThrow({
- where: {
- id: integration.authMethodId,
- },
- })
- : undefined;
-
- if (!authMethod) {
- throw new Error(
- `Auth method ${integration.authMethodId} not found for integration ${definition.id}`
- );
- }
-
- return {
- definition: {
- identifier: definition.id,
- name: definition.name,
- },
- authMethod: {
- name: authMethod.name,
- description: authMethod.description,
- type: authMethod.type,
- client: authMethod.client as ApiAuthenticationMethodOAuth2["client"],
- config: authMethod.config as ApiAuthenticationMethodOAuth2["config"],
- scopes: authMethod.scopes as ApiAuthenticationMethodOAuth2["scopes"],
- },
- };
- }
-
- #buildCallbackUrl({
- authenticationMethod,
- url,
- hasCustomClient,
- clientId,
- }: {
- authenticationMethod: ApiAuthenticationMethodOAuth2;
- url: URL;
- hasCustomClient: boolean;
- clientId: string;
- }) {
- return new URL(
- `/oauth2/callback`,
- authenticationMethod.config.appHostEnvName
- ? process.env[authenticationMethod.config.appHostEnvName] ?? url
- : url
- ).href;
- }
-
- #getMetadataFromToken({
- authenticationMethod,
- token,
- }: {
- authenticationMethod: ApiAuthenticationMethodOAuth2;
- token: AccessToken;
- }) {
- const metadata: ConnectionMetadata = {};
- if (authenticationMethod.config.token.metadata.accountPointer) {
- const accountPointer = jsonpointer.compile(
- authenticationMethod.config.token.metadata.accountPointer
- );
- const account = accountPointer.get(token.raw ?? {});
- if (typeof account === "string") {
- metadata.account = account;
- }
- }
-
- return metadata;
- }
-
- #getExpiresAtFromToken({ token }: { token: AccessToken }) {
- if (token.expiresIn) {
- return new Date(new Date().getTime() + token.expiresIn * 1000);
- }
- return undefined;
- }
-
- async #scheduleRefresh(
- expiresAt: Date | undefined,
- connection: IntegrationConnection,
- tx?: PrismaClientOrTransaction
- ) {
- if (expiresAt) {
- await workerQueue.enqueue(
- "refreshOAuthToken",
- {
- organizationId: connection.organizationId,
- connectionId: connection.id,
- },
- {
- //attempt refreshing 5 minutes before the token expires
- runAt: new Date(expiresAt.getTime() - tokenRefreshThreshold * 1000),
- tx,
- }
- );
- }
- }
-}
-
-function secretStoreKeyForToken(integrationIdentifier: string, hashedAccessToken: string) {
- return `connection/token/${integrationIdentifier}-${hashedAccessToken}`;
-}
-
-export const integrationAuthRepository = new IntegrationAuthRepository();
diff --git a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts
deleted file mode 100644
index 0ec27874e2..0000000000
--- a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { airtable } from "./integrations/airtable";
-import { github } from "./integrations/github";
-import { linear } from "./integrations/linear";
-import { openai } from "./integrations/openai";
-import { plain } from "./integrations/plain";
-import { replicate } from "./integrations/replicate";
-import { resend } from "./integrations/resend";
-import { sendgrid } from "./integrations/sendgrid";
-import { shopify } from "./integrations/shopify";
-import { slack } from "./integrations/slack";
-import { stripe } from "./integrations/stripe";
-import { supabase, supabaseManagement } from "./integrations/supabase";
-import { typeform } from "./integrations/typeform";
-import type { Integration } from "./types";
-
-export class IntegrationCatalog {
- #integrations: Record;
-
- constructor(integrations: Record) {
- this.#integrations = integrations;
- }
-
- public getIntegrations() {
- return this.#integrations;
- }
-
- public getIntegration(identifier: string) {
- const api = this.#integrations[identifier];
- if (!api) {
- return undefined;
- }
- return api;
- }
-}
-
-export const integrationCatalog = new IntegrationCatalog({
- airtable,
- github,
- linear,
- openai,
- plain,
- replicate,
- resend,
- shopify,
- slack,
- stripe,
- supabaseManagement,
- supabase,
- sendgrid,
- typeform,
-});
diff --git a/apps/webapp/app/services/externalApis/integrationConnectionCreated.server.ts b/apps/webapp/app/services/externalApis/integrationConnectionCreated.server.ts
deleted file mode 100644
index df03a86d31..0000000000
--- a/apps/webapp/app/services/externalApis/integrationConnectionCreated.server.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { MISSING_CONNECTION_RESOLVED_NOTIFICATION } from "@trigger.dev/core";
-import { PrismaClientOrTransaction, prisma } from "~/db.server";
-import { IngestSendEvent } from "../events/ingestSendEvent.server";
-import { logger } from "../logger.server";
-import { workerQueue } from "../worker.server";
-
-export class IntegrationConnectionCreatedService {
- #prismaClient: PrismaClientOrTransaction;
-
- constructor(prismaClient: PrismaClientOrTransaction = prisma) {
- this.#prismaClient = prismaClient;
- }
-
- public async call(id: string) {
- logger.debug("IntegrationConnectionCreatedService.call", { id });
-
- // first, deliver the event through the dispatcher
- const connection = await this.#prismaClient.integrationConnection.findUniqueOrThrow({
- where: {
- id,
- },
- include: {
- externalAccount: true,
- integration: true,
- },
- });
-
- const missingConnection = await this.#prismaClient.missingConnection.findUnique({
- where: {
- integrationId_connectionType_accountIdentifier: {
- integrationId: connection.integrationId,
- connectionType: connection.connectionType,
- accountIdentifier: connection.externalAccount
- ? connection.externalAccount.id
- : "DEVELOPER",
- },
- },
- include: {
- runs: {
- include: {
- queue: true,
- environment: {
- include: {
- project: true,
- organization: true,
- },
- },
- },
- orderBy: {
- createdAt: "asc",
- },
- },
- integration: true,
- externalAccount: true,
- },
- });
-
- if (!missingConnection) {
- return;
- }
-
- if (missingConnection.resolved) {
- return;
- }
-
- const firstRun = missingConnection.runs[0];
-
- if (!firstRun) {
- return;
- }
-
- const eventId = `${missingConnection.id}-resolved`;
-
- const eventService = new IngestSendEvent();
-
- await eventService.call(firstRun.environment, {
- id: eventId,
- name: MISSING_CONNECTION_RESOLVED_NOTIFICATION,
- payload: {
- id: missingConnection.id,
- type: missingConnection.connectionType,
- client: {
- id: missingConnection.integration.slug,
- title: missingConnection.integration.title,
- scopes: missingConnection.integration.scopes,
- createdAt: missingConnection.integration.createdAt,
- updatedAt: missingConnection.integration.updatedAt,
- },
- expiresAt: connection.expiresAt ?? undefined,
- account: missingConnection.externalAccount
- ? {
- id: missingConnection.externalAccount.identifier,
- metadata: missingConnection.externalAccount.metadata,
- }
- : undefined,
- },
- context: {},
- });
-
- await this.#prismaClient.missingConnection.delete({
- where: {
- id: missingConnection.id,
- },
- });
-
- for (const run of missingConnection.runs) {
- logger.debug("[IntegrationConnectionCreatedService] restarting run", {
- run,
- });
-
- // We need to start the run again
- await workerQueue.enqueue("startRun", {
- id: run.id,
- });
- }
- }
-}
diff --git a/apps/webapp/app/services/externalApis/integrations/airtable.ts b/apps/webapp/app/services/externalApis/integrations/airtable.ts
deleted file mode 100644
index 57f484f444..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/airtable.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import type { HelpSample, Integration } from "../types";
-
-function usageSample(hasApiKey: boolean): HelpSample {
- return {
- title: "Using the client",
- code: `
-import { Airtable } from "@trigger.dev/airtable";
-
-const airtable = new Airtable({
- id: "__SLUG__"${hasApiKey ? ",\n token: process.env.AIRTABLE_TOKEN!" : ""}
-});
-
-//you can define your Airtable table types
-type LaunchGoalsAndOkRs = {
- "Launch goals"?: string;
- DRI?: Collaborator;
- Team?: string;
- Status?: "On track" | "In progress" | "At risk";
- "Key results"?: Array;
- "Features (from 💻 Features table)"?: Array;
- "Status (from 💻 Features)": Array<"Live" | "Complete" | "In progress" | "Planning" | "In reviews">;
-};
-
-client.defineJob({
- id: "airtable-example-1",
- name: "Airtable Example 1: getRecords",
- version: "0.1.0",
- trigger: eventTrigger({
- name: "airtable.example",
- schema: z.object({
- baseId: z.string(),
- tableName: z.string(),
- }),
- }),
- integrations: {
- airtable,
- },
- run: async (payload, io, ctx) => {
- //then you can set the types for your table, so you get type safety
- const table = io.airtable.base(payload.baseId).table(payload.tableName);
-
- const records = await table.getRecords("muliple records", { fields: ["Status"] });
- //this will be type checked
- await io.logger.log(records[0].fields.Status ?? "no status");
- },
-});
- `,
- };
-}
-
-export const airtable: Integration = {
- identifier: "airtable",
- name: "Airtable",
- packageName: "@trigger.dev/airtable",
- authenticationMethods: {
- oauth2: {
- name: "OAuth2",
- type: "oauth2",
- client: {
- id: {
- envName: "CLOUD_AIRTABLE_CLIENT_ID",
- },
- secret: {
- envName: "CLOUD_AIRTABLE_CLIENT_SECRET",
- },
- },
- config: {
- authorization: {
- url: "https://airtable.com/oauth2/v1/authorize",
- scopeSeparator: " ",
- authorizationLocation: "header",
- extraParameters: {
- response_type: "code",
- },
- },
- token: {
- url: "https://airtable.com/oauth2/v1/token",
- metadata: {},
- },
- refresh: {
- url: "https://airtable.com/oauth2/v1/token",
- },
- },
- scopes: [
- {
- name: "data.records:read",
- description: "See the data in records",
- defaultChecked: true,
- },
- {
- name: "data.records:write",
- description: "Create, edit, and delete records",
- defaultChecked: true,
- },
- {
- name: "data.recordComments:read",
- description: "See comments in records",
- defaultChecked: true,
- },
- {
- name: "data.recordComments:write",
- description: "Create, edit, and delete record comments",
- defaultChecked: true,
- },
- {
- name: "schema.bases:read",
- description: "See the structure of a base, like table names or field types",
- },
- {
- name: "schema.bases:write",
- description: "Edit the structure of a base, like adding new fields or tables",
- },
- {
- name: "webhook:manage",
- description:
- "View, create, delete webhooks for a base, as well as fetch webhook payloads.",
- defaultChecked: true,
- },
- ],
- help: {
- samples: [usageSample(false)],
- },
- },
- apiKey: {
- type: "apikey",
- help: {
- samples: [usageSample(true)],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/github.ts b/apps/webapp/app/services/externalApis/integrations/github.ts
deleted file mode 100644
index e2c0e5bfcb..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/github.ts
+++ /dev/null
@@ -1,335 +0,0 @@
-import type { HelpSample, Integration, ScopeAnnotation } from "../types";
-
-const repoAnnotation: ScopeAnnotation = {
- label: "Repo",
-};
-
-const webhookAnnotation: ScopeAnnotation = {
- label: "Webhooks",
-};
-
-const orgAnnotation: ScopeAnnotation = {
- label: "Orgs",
-};
-
-const keysAnnotation: ScopeAnnotation = {
- label: "Keys",
-};
-
-const userAnnotation: ScopeAnnotation = {
- label: "User",
-};
-
-const usageSample: HelpSample = {
- title: "Using the client",
- code: `
-import { Github, events } from "@trigger.dev/github";
-
-const github = new Github({
- id: "__SLUG__",
- token: process.env.GITHUB_TOKEN!,
-});
-
-client.defineJob({
- id: "github-integration-on-issue-opened",
- name: "GitHub Integration - On Issue Opened",
- version: "0.1.0",
- integrations: { github },
- trigger: github.triggers.repo({
- event: events.onIssueOpened,
- owner: "triggerdotdev",
- repo: "empty",
- }),
- run: async (payload, io, ctx) => {
- await io.github.addIssueAssignees("add assignee", {
- owner: payload.repository.owner.login,
- repo: payload.repository.name,
- issueNumber: payload.issue.number,
- assignees: ["matt-aitken"],
- });
-
- await io.github.addIssueLabels("add label", {
- owner: payload.repository.owner.login,
- repo: payload.repository.name,
- issueNumber: payload.issue.number,
- labels: ["bug"],
- });
-
- return { payload, ctx };
- },
-});
-
- `,
-};
-
-export const github: Integration = {
- identifier: "github",
- name: "GitHub",
- packageName: "@trigger.dev/github@latest",
- authenticationMethods: {
- oauth2: {
- name: "OAuth",
- type: "oauth2",
- client: {
- id: {
- envName: "CLOUD_GITHUB_CLIENT_ID",
- },
- secret: {
- envName: "CLOUD_GITHUB_CLIENT_SECRET",
- },
- },
- config: {
- authorization: {
- url: "https://github.com/login/oauth/authorize",
- scopeSeparator: " ",
- },
- token: {
- url: "https://github.com/login/oauth/access_token",
- metadata: {
- accountPointer: "/team/name",
- },
- },
- refresh: {
- url: "https://github.com/login/oauth/authorize",
- },
- },
- scopes: [
- {
- name: "repo",
- description:
- "Grants full access to public and private repositories including read and write access to code, commit statuses, repository invitations, collaborators, deployment statuses, and repository webhooks. Note: In addition to repository related resources, the repo scope also grants access to manage organization-owned resources including projects, invitations, team memberships and webhooks. This scope also grants the ability to manage projects owned by users.",
- annotations: [repoAnnotation],
- },
-
- {
- name: "repo:status",
- description:
- "Grants read/write access to commit statuses in public and private repositories. This scope is only necessary to grant other users or services access to private repository commit statuses without granting access to the code.",
- annotations: [repoAnnotation],
- },
-
- {
- name: "repo_deployment",
- description:
- "Grants access to deployment statuses for public and private repositories. This scope is only necessary to grant other users or services access to deployment statuses, without granting access to the code.",
- annotations: [repoAnnotation],
- },
-
- {
- name: "public_repo",
- description:
- "Limits access to public repositories. That includes read/write access to code, commit statuses, repository projects, collaborators, and deployment statuses for public repositories and organizations. Also required for starring public repositories.",
- annotations: [repoAnnotation],
- },
-
- {
- name: "repo:invite",
- description:
- "Grants accept/decline abilities for invitations to collaborate on a repository. This scope is only necessary to grant other users or services access to invites without granting access to the code.",
- annotations: [repoAnnotation],
- },
-
- {
- name: "delete_repo",
- description: "Grants access to delete adminable repositories.",
- annotations: [repoAnnotation],
- },
-
- {
- name: "security_events",
- description:
- "Grants read and write access to security events in the code scanning API. This scope is only necessary to grant other users or services access to security events without granting access to the code.",
- },
-
- {
- name: "admin:repo_hook",
- description:
- "Grants read, write, ping, and delete access to repository hooks in public or private repositories. The repo and public_repo scopes grant full access to repositories, including repository hooks. Use the admin:repo_hook scope to limit access to only repository hooks.",
- defaultChecked: true,
- annotations: [webhookAnnotation],
- },
- {
- name: "write:repo_hook",
- description:
- "Grants read, write, and ping access to hooks in public or private repositories.",
- annotations: [webhookAnnotation],
- },
- {
- name: "read:repo_hook",
- description: "Grants read and ping access to hooks in public or private repositories.",
- annotations: [webhookAnnotation],
- },
-
- {
- name: "admin:org",
- description: "Fully manage the organization and its teams, projects, and memberships.",
- annotations: [orgAnnotation],
- },
- {
- name: "write:org",
- description:
- "Read and write access to organization membership, organization projects, and team membership.",
- annotations: [orgAnnotation],
- },
-
- {
- name: "read:org",
- description:
- "Read-only access to organization membership, organization projects, and team membership.",
- annotations: [orgAnnotation],
- },
-
- {
- name: "admin:public_key",
- description: "Fully manage public keys.",
- annotations: [keysAnnotation],
- },
-
- {
- name: "write:public_key",
- description: "Create, list, and view details for public keys.",
- annotations: [keysAnnotation],
- },
- {
- name: "read:public_key",
- description: "List and view details for public keys.",
- annotations: [keysAnnotation],
- },
-
- {
- name: "admin:org_hook",
- description:
- "Grants read, write, ping, and delete access to organization hooks. Note: OAuth tokens will only be able to perform these actions on organization hooks which were created by the OAuth App. Personal access tokens will only be able to perform these actions on organization hooks created by a user.",
- annotations: [orgAnnotation, webhookAnnotation],
- },
-
- {
- name: "gist",
- description: "Grants write access to gists.",
- },
- {
- name: "notifications",
- description:
- "Grants read access to a user's notifications, mark as read access to threads, watch and unwatch access to a repository, and read, write, and delete access to thread subscriptions.",
- },
- {
- name: "user",
- description:
- " Grants read/write access to profile info only. Note that this scope includes user:email and user:follow.",
- annotations: [userAnnotation],
- },
- {
- name: "read:user",
- description: "Grants read access to a user's profile data.",
- annotations: [userAnnotation],
- },
-
- {
- name: "user:email",
- description: "Grants read access to a user's email addresses.",
- annotations: [userAnnotation],
- },
- {
- name: "user:follow",
- description: "Grants access to follow or unfollow other users.",
- annotations: [userAnnotation],
- },
- {
- name: "project",
- description: "Grants read/write access to user and organization projects.",
- },
-
- {
- name: "read:project",
- description: "Grants read only access to user and organization projects.",
- },
-
- {
- name: "write:discussion",
- description: "Allows read and write access for team discussions.",
- },
-
- {
- name: "read:discussion",
- description: "Allows read access for team discussions.",
- },
-
- {
- name: "write:packages",
- description: "Grants access to upload or publish a package in GitHub Packages.",
- },
-
- {
- name: "read:packages",
- description: "Grants access to download or install packages from GitHub Packages.",
- },
-
- {
- name: "delete:packages",
- description: "Grants access to delete packages from GitHub Packages.",
- },
-
- {
- name: "admin:gpg_key",
- description: "Fully manage GPG keys.",
- },
-
- {
- name: "write:gpg_key",
- description: "Create, list, and view details for GPG keys.",
- },
-
- {
- name: "read:gpg_key",
- description: "List and view details for GPG keys.",
- },
-
- {
- name: "codespace",
- description:
- "Grants the ability to create and manage codespaces. Codespaces can expose a GITHUB_TOKEN which may have a different set of scopes",
- },
-
- {
- name: "workflow",
- description:
- "Grants the ability to add and update GitHub Actions workflow files. Workflow files can be committed without this scope if the same file (with both the same path and contents) exists on another branch in the same repository. Workflow files can expose GITHUB_TOKEN which may have a different set of scopes.",
- },
- ],
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Github } from "@trigger.dev/github";
-
-const github = new Github({
- id: "__SLUG__"
-});
-`,
- },
- usageSample,
- ],
- },
- },
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Github } from "@trigger.dev/github";
-
-const github = new Github({
- id: "__SLUG__",
- token: process.env.GITHUB_TOKEN!
-});
-`,
- },
- usageSample,
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts
deleted file mode 100644
index 841ded6538..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/linear.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import type { HelpSample, Integration } from "../types";
-
-function usageSample(hasApiKey: boolean): HelpSample {
- return {
- title: "Using the client",
- code: `
-import { Linear } from "@trigger.dev/linear";
-
-const linear = new Linear({
- id: "__SLUG__",${hasApiKey ? "\n apiKey: process.env.LINEAR_API_KEY!," : ""}
-});
-
-client.defineJob({
- id: "linear-react-to-new-issue",
- name: "Linear - React To New Issue",
- version: "0.1.0",
- integrations: { linear },
- trigger: linear.onIssueCreated(),
- run: async (payload, io, ctx) => {
- await io.linear.createComment("create-comment", {
- issueId: payload.data.id,
- body: "Thank's for opening this issue!"
- });
-
- await io.linear.createReaction("create-reaction", {
- issueId: payload.data.id,
- emoji: "+1"
- });
-
- return { payload, ctx };
- },
-});
- `,
- };
-}
-
-export const linear: Integration = {
- identifier: "linear",
- name: "Linear",
- packageName: "@trigger.dev/linear@latest",
- authenticationMethods: {
- oauth2: {
- name: "OAuth",
- type: "oauth2",
- client: {
- id: {
- envName: "CLOUD_LINEAR_CLIENT_ID",
- },
- secret: {
- envName: "CLOUD_LINEAR_CLIENT_SECRET",
- },
- },
- config: {
- authorization: {
- url: "https://linear.app/oauth/authorize",
- scopeSeparator: ",",
- },
- token: {
- url: "https://api.linear.app/oauth/token",
- metadata: {},
- },
- refresh: {
- url: "https://linear.app/oauth/authorize",
- },
- pkce: false,
- },
- scopes: [
- {
- name: "read",
- description: "Read access for the user's account. This scope must always be present.",
- defaultChecked: true,
- },
- {
- name: "write",
- description:
- "Grants global write access to the user's account. Use a more targeted scope if you don't need full access.",
- defaultChecked: true,
- },
-
- {
- name: "issues:create",
- description: "Grants access to create issues and attachments only.",
- annotations: [{ label: "Issues" }],
- },
-
- {
- name: "comments:create",
- description: "Grants access to create new issue comments.",
- annotations: [{ label: "Comments" }],
- },
-
- {
- name: "admin",
- description:
- "Grants full access to admin-level endpoints. Don't use this unless you really need it.",
- },
- ],
- help: {
- samples: [usageSample(false)],
- },
- },
- apikey: {
- type: "apikey",
- help: {
- samples: [usageSample(true)],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/openai.ts b/apps/webapp/app/services/externalApis/integrations/openai.ts
deleted file mode 100644
index 952ef35f1a..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/openai.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { highlight } from "prismjs";
-import type { Integration } from "../types";
-
-export const openai: Integration = {
- identifier: "openai",
- name: "OpenAI",
- description: "You can perform very long completions with the integration",
- packageName: "@trigger.dev/openai@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { OpenAI } from "@trigger.dev/openai";
-
-const openai = new OpenAI({
- id: "__SLUG__",
- apiKey: process.env.OPENAI_API_KEY!,
-});
-`,
- },
- {
- title: "Using the client",
- code: `
-client.defineJob({
- id: "openai-tasks",
- name: "OpenAI Tasks",
- version: "0.0.1",
- trigger: eventTrigger({
- name: "openai.tasks",
- schema: z.object({}),
- }),
- integrations: {
- openai,
- },
- run: async (payload, io, ctx) => {
- //this background function can take longer than a serverless timeout
- const response = await io.openai.backgroundCreateChatCompletion(
- "background-chat-completion",
- {
- model: "gpt-3.5-turbo",
- messages: [
- {
- role: "user",
- content: "Create a good programming joke about background jobs",
- },
- ],
- }
- );
-
- await io.logger.info("choices", response.choices);
- },
-});
- `,
- highlight: [
- [10, 10],
- [13, 24],
- ],
- },
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/plain.ts b/apps/webapp/app/services/externalApis/integrations/plain.ts
deleted file mode 100644
index 0948ba381c..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/plain.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import type { Integration } from "../types";
-
-export const plain: Integration = {
- identifier: "plain",
- name: "Plain",
- packageName: "@trigger.dev/plain@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Plain } from "@trigger.dev/plain";
-
-export const plain = new Plain({
- id: "__SLUG__",
- apiKey: process.env.PLAIN_API_KEY!,
-});
-`,
- },
- {
- title: "Using the client",
- code: `
-client.defineJob({
- id: "plain-playground",
- name: "Plain Playground",
- version: "0.1.1",
- integrations: {
- plain,
- },
- trigger: eventTrigger({
- name: "plain.playground",
- }),
- run: async (payload, io, ctx) => {
- const { customer } = await io.plain.upsertCustomer("upsert-customer", {
- identifier: {
- emailAddress: "rick.astley@gmail.com",
- },
- onCreate: {
- email: {
- email: "rick.astley@gmail.com",
- isVerified: true,
- },
- fullName: "Rick Astley",
- externalId: "u_123",
- },
- onUpdate: {
- fullName: {
- value: "Rick Astley",
- },
- externalId: {
- value: "u_123",
- },
- },
- });
-
- const foundCustomer = await io.plain.getCustomerById("get-customer", {
- customerId: customer.id,
- });
-
- const timelineEntry = await io.plain.upsertCustomTimelineEntry(
- "upsert-timeline-entry",
- {
- customerId: customer.id,
- title: "My timeline entry",
- components: [
- {
- componentText: {
- text: \`This is a nice title\`,
- },
- },
- {
- componentDivider: {
- dividerSpacingSize: ComponentDividerSpacingSize.M,
- },
- },
- {
- componentText: {
- textSize: ComponentTextSize.S,
- textColor: ComponentTextColor.Muted,
- text: "External id",
- },
- },
- {
- componentText: {
- text: foundCustomer?.externalId ?? "",
- },
- },
- ],
- }
- );
- },
-});
- `,
- highlight: [[12, 68]],
- },
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/replicate.ts b/apps/webapp/app/services/externalApis/integrations/replicate.ts
deleted file mode 100644
index 095a043e51..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/replicate.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { HelpSample, Integration } from "../types";
-
-function usageSample(hasApiKey: boolean): HelpSample {
- const apiKeyPropertyName = "apiKey";
-
- return {
- title: "Using the client",
- code: `
-import { Replicate } from "@trigger.dev/replicate";
-
-const replicate = new Replicate({
- id: "__SLUG__",${hasApiKey ? `\n ${apiKeyPropertyName}: process.env.REPLICATE_API_KEY!,` : ""}
-});
-
-client.defineJob({
- id: "replicate-create-prediction",
- name: "Replicate - Create Prediction",
- version: "0.1.0",
- integrations: { replicate },
- trigger: eventTrigger({
- name: "replicate.predict",
- schema: z.object({
- prompt: z.string(),
- version: z.string(),
- }),
- }),
- run: async (payload, io, ctx) => {
- return io.replicate.predictions.createAndAwait("await-prediction", {
- version: payload.version,
- input: { prompt: payload.prompt },
- });
- },
-});
- `,
- };
-}
-
-export const replicate: Integration = {
- identifier: "replicate",
- name: "Replicate",
- packageName: "@trigger.dev/replicate@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [usageSample(true)],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/resend.ts b/apps/webapp/app/services/externalApis/integrations/resend.ts
deleted file mode 100644
index bacf74624c..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/resend.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import type { Integration } from "../types";
-
-export const resend: Integration = {
- identifier: "resend",
- name: "Resend",
- packageName: "@trigger.dev/resend@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Resend } from "@trigger.dev/resend";
-
-const resend = new Resend({
- id: "__SLUG__",
- apiKey: process.env.RESEND_API_KEY!,
-});
-`,
- },
- {
- title: "Using the client",
- code: `
-client.defineJob({
- id: "send-resend-email",
- name: "Send Resend Email",
- version: "0.1.0",
- trigger: eventTrigger({
- name: "send.email",
- schema: z.object({
- to: z.union([z.string(), z.array(z.string())]),
- subject: z.string(),
- text: z.string(),
- }),
- }),
- integrations: {
- resend,
- },
- run: async (payload, io, ctx) => {
- await io.resend.sendEmail("send-email", {
- to: payload.to,
- subject: payload.subject,
- text: payload.text,
- from: "Trigger.dev ",
- });
- },
-});
- `,
- highlight: [
- [13, 15],
- [17, 22],
- ],
- },
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/sendgrid.ts b/apps/webapp/app/services/externalApis/integrations/sendgrid.ts
deleted file mode 100644
index ecda538748..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/sendgrid.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import type { Integration } from "../types";
-
-export const sendgrid: Integration = {
- identifier: "sendgrid",
- name: "SendGrid",
- packageName: "@trigger.dev/sendgrid@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { SendGrid } from "@trigger.dev/sendgrid";
-
-const sendgrid = new SendGrid({
- id: "__SLUG__",
- apiKey: process.env.SENDGRID_API_KEY!,
-});
-`,
- },
- {
- title: "Using the client",
- code: `
-client.defineJob({
- id: "send-sendgrid-email",
- name: "Send SendGrid Email",
- version: "0.1.0",
- trigger: eventTrigger({
- name: "send.email",
- schema: z.object({
- to: z.string(),
- subject: z.string(),
- text: z.string(),
- }),
- }),
- integrations: {
- sendgrid,
- },
- run: async (payload, io, ctx) => {
- await io.sendgrid.sendEmail({
- to: payload.to,
- from: "Trigger.dev ",
- subject: payload.subject,
- text: payload.text,
- });
- },
-});
- `,
- highlight: [
- [13, 15],
- [17, 22],
- ],
- },
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/shopify.ts b/apps/webapp/app/services/externalApis/integrations/shopify.ts
deleted file mode 100644
index 07c4408ecd..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/shopify.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { HelpSample, Integration } from "../types";
-
-function usageSample(hasApiKey: boolean): HelpSample {
- const apiKeyPropertyName = "apiKey";
-
- return {
- title: "Using the client",
- code: `
-import { Shopify } from "@trigger.dev/shopify";
-
-const shopify = new Shopify({
- id: "__SLUG__",${hasApiKey ? `\n ${apiKeyPropertyName}: process.env.SHOPIFY_API_KEY!,` : ""}
- apiSecretKey: process.env.SHOPIFY_API_SECRET_KEY!,
- adminAccessToken: process.env.SHOPIFY_ADMIN_ACCESS_TOKEN!,
- hostName: process.env.SHOPIFY_SHOP_DOMAIN!,
-});
-
-client.defineJob({
- id: "shopify-create-product",
- name: "Shopify: Create Product",
- version: "0.1.0",
- integrations: { shopify },
- trigger: eventTrigger({
- name: "shopify.product.create",
- schema: z.object({
- title: z.string(),
- }),
- }),
- run: async (payload, io, ctx) => {
- const product = await io.shopify.rest.Product.save("create-product", {
- fromData: {
- title: payload.title,
- },
- });
-
- await io.logger.info(\`Created product \${product.id}: \${product.title}\`);
- },
-});
- `,
- };
-}
-
-export const shopify: Integration = {
- identifier: "shopify",
- name: "Shopify",
- packageName: "@trigger.dev/shopify@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [usageSample(true)],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/slack.ts b/apps/webapp/app/services/externalApis/integrations/slack.ts
deleted file mode 100644
index 31118eac66..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/slack.ts
+++ /dev/null
@@ -1,863 +0,0 @@
-import type { Help, Integration } from "../types";
-
-const help: Help = {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Slack } from "@trigger.dev/slack";
-
-const slack = new Slack({
- id: "__SLUG__",
-});
-`,
- },
- {
- title: "Using the client",
- code: `
-client.defineJob({
- id: "slack-test",
- name: "Slack test",
- version: "0.0.1",
- trigger: eventTrigger({
- name: "slack.test",
- schema: z.object({}),
- }),
- integrations: {
- slack,
- },
- run: async (payload, io, ctx) => {
- const response = await io.slack.postMessage("post message", {
- channel: "C04GWUTDC3W",
- text: "My first Slack message",
- });
- },
-});
- `,
- highlight: [
- [9, 11],
- [13, 16],
- ],
- },
- ],
-};
-
-export const slack: Integration = {
- identifier: "slack",
- name: "Slack",
- packageName: "@trigger.dev/slack@latest",
- authenticationMethods: {
- oauth2Bot: {
- name: "OAuth2 (Bot)",
- description: "Authenticate as a bot. This is the recommended method.",
- type: "oauth2",
- client: {
- id: {
- envName: "CLOUD_SLACK_CLIENT_ID",
- },
- secret: {
- envName: "CLOUD_SLACK_CLIENT_SECRET",
- },
- },
- config: {
- authorization: {
- url: "https://slack.com/oauth/v2/authorize",
- scopeSeparator: ",",
- },
- token: {
- url: "https://slack.com/api/oauth.v2.access",
- metadata: {
- accountPointer: "/team/name",
- },
- },
- refresh: {
- url: "https://slack.com/api/oauth.v2.access",
- },
- appHostEnvName: "CLOUD_SLACK_APP_HOST",
- },
- scopes: [
- {
- name: "app_mentions:read",
- description:
- "View messages that directly mention @your_slack_app in conversations that the app is in",
- },
-
- {
- name: "bookmarks:read",
- description: "List bookmarks",
- },
-
- {
- name: "bookmarks:write",
- description: "Create, edit, and remove bookmarks",
- },
-
- {
- name: "calls:read",
- description: "View information about ongoing and past calls",
- },
-
- {
- name: "calls:write",
- description: "Start and manage calls in a workspace",
- },
-
- {
- name: "channels:history",
- description:
- "View messages and other content in public channels that your slack app has been added to",
- },
-
- {
- name: "channels:join",
- description: "Join public channels in a workspace",
- defaultChecked: true,
- },
- {
- name: "channels:manage",
- description:
- "Manage public channels that your slack app has been added to and create new ones",
- },
- {
- name: "channels:read",
- description: "View basic information about public channels in a workspace",
- },
-
- {
- name: "channels:write",
- description: "Manage a user’s public channels and create new ones on a user’s behalf",
- },
- {
- name: "channels:write.invites",
- description: "Invite members to public channels",
- },
-
- {
- name: "channels:write.topic",
- description: "Set the description of public channels",
- },
-
- {
- name: "chat:write",
- description: "Post messages in approved channels & conversations",
- defaultChecked: true,
- },
-
- {
- name: "chat:write.customize",
- description: "Send messages as @your_slack_app with a customized username and avatar",
- defaultChecked: true,
- },
- {
- name: "chat:write.public",
- description: "Send messages to channels @your_slack_app isn't a member of",
- defaultChecked: true,
- },
-
- {
- name: "commands",
- description: "Add shortcuts and/or slash commands that people can use",
- },
-
- {
- name: "conversations.connect:manage",
- description: "Allows your slack app to manage Slack Connect channels",
- },
- {
- name: "conversations.connect:read",
- description:
- "Receive Slack Connect invite events sent to the channels your slack app is in",
- },
- {
- name: "conversations.connect:write",
- description:
- "Create Slack Connect invitations for channels that your slack app has been added to, and accept invitations sent to your slack app",
- },
- {
- name: "dnd:read",
- description: "View Do Not Disturb settings for people in a workspace",
- },
-
- {
- name: "emoji:read",
- description: "View custom emoji in a workspace",
- },
-
- {
- name: "files:read",
- description:
- "View files shared in channels and conversations that your slack app has been added to",
- },
-
- {
- name: "files:write",
- description: "Upload, edit, and delete files as your slack app",
- },
-
- {
- name: "groups:history",
- description:
- "View messages and other content in private channels that your slack app has been added to",
- },
-
- {
- name: "groups:read",
- description:
- "View basic information about private channels that your slack app has been added to",
- },
-
- {
- name: "groups:write",
- description:
- "Manage private channels that your slack app has been added to and create new ones",
- },
-
- {
- name: "groups:write.invites",
- description: "Invite members to private channels",
- },
-
- {
- name: "groups:write.topic",
- description: "Set the description of private channels",
- },
-
- {
- name: "im:history",
- description:
- "View messages and other content in direct messages that your slack app has been added to",
- },
-
- {
- name: "im:read",
- description:
- "View basic information about direct messages that your slack app has been added to",
- },
-
- {
- name: "im:write",
- description: "Start direct messages with people",
- },
-
- {
- name: "incoming-webhook",
- description: "Create one-way webhooks to post messages to a specific channel",
- },
-
- {
- name: "links.embed:write",
- description: "Embed video player URLs in messages and app surfaces",
- },
-
- {
- name: "links:read",
- description: "View URLs in messages",
- },
-
- {
- name: "links:write",
- description: "Show previews of URLs in messages",
- },
-
- {
- name: "metadata.message:read",
- description:
- "Allows your slack app to read message metadata in channels that your slack app has been added to",
- },
- {
- name: "mpim:history",
- description:
- "View messages and other content in group direct messages that your slack app has been added to",
- },
-
- {
- name: "mpim:read",
- description:
- "View basic information about group direct messages that your slack app has been added to",
- },
-
- {
- name: "mpim:write",
- description: "Start group direct messages with people",
- },
-
- {
- name: "mpim:write.invites",
- description: "Invite members to group direct messages",
- },
-
- {
- name: "mpim:write.topic",
- description: "Set the description in group direct messages",
- },
-
- {
- name: "none",
- description: "Execute methods without needing a scope",
- },
-
- {
- name: "pins:read",
- description:
- "View pinned content in channels and conversations that your slack app has been added to",
- },
-
- {
- name: "pins:write",
- description: "Add and remove pinned messages and files",
- },
-
- {
- name: "reactions:read",
- description:
- "View emoji reactions and their associated content in channels and conversations that your slack app has been added to",
- },
-
- {
- name: "reactions:write",
- description: "Add and edit emoji reactions",
- },
-
- {
- name: "reminders:read",
- description: "View reminders created by your slack app",
- },
-
- {
- name: "reminders:write",
- description: "Add, remove, or mark reminders as complete",
- },
-
- {
- name: "remote_files:read",
- description: "View remote files added by the app in a workspace",
- },
-
- {
- name: "remote_files:share",
- description: "Share remote files on a user’s behalf",
- },
-
- {
- name: "remote_files:write",
- description: "Add, edit, and delete remote files on a user’s behalf",
- },
-
- {
- name: "search:read.public",
- description: "Search a workspace's messages in public channels",
- },
-
- {
- name: "team.billing:read",
- description:
- "Allows your slack app to read the billing plan for workspaces your slack app has been installed to",
- },
-
- {
- name: "team.preferences:read",
- description:
- "Allows your slack app to read the preferences for workspaces your slack app has been installed to",
- },
-
- {
- name: "team:read",
- description:
- "View the name, email domain, and icon for workspaces your slack app is connected to",
- },
-
- {
- name: "tokens.basic",
- description: "Execute methods without needing a scope",
- },
-
- {
- name: "triggers:read",
- description: "Read new Platform triggers",
- },
- {
- name: "triggers:write",
- description: "Create new Platform triggers",
- },
- {
- name: "usergroups:read",
- description: "View user groups in a workspace",
- },
-
- {
- name: "usergroups:write",
- description: "Create and manage user groups",
- },
-
- {
- name: "users.profile:read",
- description: "View profile details about people in a workspace",
- },
-
- {
- name: "users:read",
- description: "View people in a workspace",
- },
-
- {
- name: "users:read.email",
- description: "View email addresses of people in a workspace",
- },
-
- {
- name: "users:write",
- description: "Set presence for your slack app",
- },
-
- {
- name: "workflow.steps:execute",
- description: "Add steps that people can use in Workflow Builder",
- },
- ],
- help,
- },
- oauth2User: {
- name: "OAuth2 (User)",
- description: "Authenticate as a user",
- type: "oauth2",
- client: {
- id: {
- envName: "CLOUD_SLACK_CLIENT_ID",
- },
- secret: {
- envName: "CLOUD_SLACK_CLIENT_SECRET",
- },
- },
- config: {
- authorization: {
- url: "https://slack.com/oauth/v2/authorize",
- scopeSeparator: ",",
- scopeParamName: "user_scope",
- },
- token: {
- url: "https://slack.com/api/oauth.v2.access",
- metadata: {
- accountPointer: "/team/name",
- },
- accessTokenPointer: "/authed_user/access_token",
- scopePointer: "/authed_user/scope",
- },
- refresh: {
- url: "https://slack.com/api/oauth.v2.access",
- },
- appHostEnvName: "CLOUD_SLACK_APP_HOST",
- },
- scopes: [
- {
- name: "admin",
- description: "Administer a workspace",
- },
- {
- name: "admin.analytics:read",
- description: "Access analytics data about the organization",
- },
- {
- name: "admin.apps:read",
- description: "View apps and app requests in a workspace",
- },
- {
- name: "admin.apps:write",
- description: "Manage apps in a workspace",
- },
- {
- name: "admin.barriers:read",
- description: "Read information barriers in the organization",
- },
- {
- name: "admin.barriers:write",
- description: "Manage information barriers in the organization",
- },
- {
- name: "admin.conversations:read",
- description: "View the channel’s member list, topic, purpose and channel name",
- },
- {
- name: "admin.conversations:write",
- description: "Start a new conversation, modify a conversation and modify channel details",
- },
- {
- name: "admin.invites:read",
- description: "Gain information about invite requests in a Grid organization.",
- },
- {
- name: "admin.invites:write",
- description: "Approve or deny invite requests in a Grid organization.",
- },
- {
- name: "admin.roles:read",
- description: "List role assignments for your workspace.",
- },
- {
- name: "admin.roles:write",
- description: "Add and remove role assignments for your workspace.",
- },
- {
- name: "admin.teams:read",
- description: "Access information about a workspace",
- },
- {
- name: "admin.teams:write",
- description: "Make changes to a workspace",
- },
- {
- name: "admin.usergroups:read",
- description: "Access information about user groups",
- },
- {
- name: "admin.usergroups:write",
- description: "Make changes to your usergroups",
- },
- {
- name: "admin.users:read",
- description: "Access a workspace’s profile information",
- },
- {
- name: "admin.users:write",
- description: "Modify account information",
- },
- {
- name: "admin.workflows:read",
- description: "View all workflows in a workspace",
- },
- {
- name: "admin.workflows:write",
- description: "Manage workflows in a workspace",
- },
-
- {
- name: "auditlogs:read",
- description: "View events from all workspaces, channels and users (Enterprise Grid only)",
- },
-
- {
- name: "bookmarks:read",
- description: "List bookmarks",
- },
-
- {
- name: "bookmarks:write",
- description: "Create, edit, and remove bookmarks",
- },
-
- {
- name: "calls:read",
- description: "View information about ongoing and past calls",
- },
-
- {
- name: "calls:write",
- description: "Start and manage calls in a workspace",
- },
-
- {
- name: "channels:history",
- description:
- "View messages and other content in public channels that your slack app has been added to",
- },
-
- {
- name: "channels:read",
- description: "View basic information about public channels in a workspace",
- },
-
- {
- name: "channels:write.invites",
- description: "Invite members to public channels",
- },
-
- {
- name: "channels:write.topic",
- description: "Set the description of public channels",
- },
-
- {
- name: "chat:write",
- description: "Post messages in approved channels & conversations",
- },
-
- {
- name: "chat:write:bot",
- description: "Send messages as your slack app",
- defaultChecked: true,
- },
- {
- name: "chat:write:user",
- description: "Send messages on a user’s behalf",
- defaultChecked: true,
- },
-
- {
- name: "commands",
- description: "Add shortcuts and/or slash commands that people can use",
- },
-
- {
- name: "dnd:read",
- description: "View Do Not Disturb settings for people in a workspace",
- },
- {
- name: "dnd:write",
- description: "Edit a user’s Do Not Disturb settings",
- },
- {
- name: "email",
- description: "View a user’s email address",
- },
-
- {
- name: "emoji:read",
- description: "View custom emoji in a workspace",
- },
-
- {
- name: "files:read",
- description:
- "View files shared in channels and conversations that your slack app has been added to",
- },
-
- {
- name: "files:write",
- description: "Upload, edit, and delete files as your slack app",
- },
- {
- name: "files:write:user",
- description: "Upload, edit, and delete files as your slack app",
- },
-
- {
- name: "groups:history",
- description:
- "View messages and other content in private channels that your slack app has been added to",
- },
- {
- name: "groups:read",
- description:
- "View basic information about private channels that your slack app has been added to",
- },
- {
- name: "groups:write",
- description:
- "Manage private channels that your slack app has been added to and create new ones",
- },
-
- {
- name: "groups:write.invites",
- description: "Invite members to private channels",
- },
-
- {
- name: "groups:write.topic",
- description: "Set the description of private channels",
- },
- {
- name: "identity.avatar",
- description: "View a user’s Slack avatar",
- },
- {
- name: "identity.basic",
- description: "View information about a user’s identity",
- },
- {
- name: "identity.email",
- description: "View a user’s email address",
- },
- {
- name: "identity.team",
- description: "View a user’s Slack workspace name",
- },
-
- {
- name: "im:history",
- description:
- "View messages and other content in direct messages that your slack app has been added to",
- },
-
- {
- name: "im:read",
- description:
- "View basic information about direct messages that your slack app has been added to",
- },
-
- {
- name: "im:write",
- description: "Start direct messages with people",
- },
-
- {
- name: "incoming-webhook",
- description: "Create one-way webhooks to post messages to a specific channel",
- },
-
- {
- name: "links.embed:write",
- description: "Embed video player URLs in messages and app surfaces",
- },
-
- {
- name: "links:read",
- description: "View URLs in messages",
- },
-
- {
- name: "links:write",
- description: "Show previews of URLs in messages",
- },
-
- {
- name: "mpim:history",
- description:
- "View messages and other content in group direct messages that your slack app has been added to",
- },
-
- {
- name: "mpim:read",
- description:
- "View basic information about group direct messages that your slack app has been added to",
- },
-
- {
- name: "mpim:write",
- description: "Start group direct messages with people",
- },
-
- {
- name: "mpim:write.invites",
- description: "Invite members to group direct messages",
- },
-
- {
- name: "mpim:write.topic",
- description: "Set the description in group direct messages",
- },
-
- {
- name: "openid",
- description: "View information about a user’s identity",
- },
-
- {
- name: "pins:read",
- description:
- "View pinned content in channels and conversations that your slack app has been added to",
- },
-
- {
- name: "pins:write",
- description: "Add and remove pinned messages and files",
- },
- {
- name: "profile",
- description: "View a user’s Slack avatar and Slack workspace's basic information",
- },
-
- {
- name: "reactions:read",
- description:
- "View emoji reactions and their associated content in channels and conversations that your slack app has been added to",
- },
-
- {
- name: "reactions:write",
- description: "Add and edit emoji reactions",
- },
-
- {
- name: "reminders:read",
- description: "View reminders created by your slack app",
- },
-
- {
- name: "reminders:write",
- description: "Add, remove, or mark reminders as complete",
- },
-
- {
- name: "remote_files:read",
- description: "View remote files added by the app in a workspace",
- },
-
- {
- name: "remote_files:share",
- description: "Share remote files on a user’s behalf",
- },
-
- {
- name: "search:read",
- description: "Search a workspace’s content",
- },
-
- {
- name: "stars:read",
- description: "View messages and files that your slack app has starred",
- },
- {
- name: "stars:write",
- description: "Add or remove stars",
- },
-
- {
- name: "team.billing:read",
- description:
- "Allows your slack app to read the billing plan for workspaces your slack app has been installed to",
- },
-
- {
- name: "team.preferences:read",
- description:
- "Allows your slack app to read the preferences for workspaces your slack app has been installed to",
- },
-
- {
- name: "team:read",
- description:
- "View the name, email domain, and icon for workspaces your slack app is connected to",
- },
-
- {
- name: "tokens.basic",
- description: "Execute methods without needing a scope",
- },
-
- {
- name: "usergroups:read",
- description: "View user groups in a workspace",
- },
-
- {
- name: "usergroups:write",
- description: "Create and manage user groups",
- },
-
- {
- name: "users.profile:read",
- description: "View profile details about people in a workspace",
- },
- {
- name: "users.profile:write",
- description: "Edit a user’s profile information and status",
- },
-
- {
- name: "users:read",
- description: "View people in a workspace",
- },
-
- {
- name: "users:read.email",
- description: "View email addresses of people in a workspace",
- },
-
- {
- name: "users:write",
- description: "Set presence for your slack app",
- },
- ],
- help,
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/stripe.ts b/apps/webapp/app/services/externalApis/integrations/stripe.ts
deleted file mode 100644
index 878f2e8bbd..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/stripe.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import type { Integration } from "../types";
-
-export const stripe: Integration = {
- identifier: "stripe",
- name: "Stripe",
- packageName: "@trigger.dev/stripe",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the integration",
- code: `
-import { Stripe } from "@trigger.dev/stripe";
-
-export const stripe = new Stripe({
- id: "__SLUG__",
- apiKey: process.env.PLAIN_API_KEY!,
-});
-`,
- },
- {
- title: "Using the integration",
- code: `
-client.defineJob({
- id: "stripe-playground",
- name: "Stripe Playground",
- version: "0.1.1",
- integrations: {
- stripe,
- },
- trigger: eventTrigger({
- name: "stripe.playground",
- }),
- run: async (payload, io, ctx) => {
- await io.stripe.createCharge("charge-customer", {
- amount: 100,
- currency: "usd",
- source: payload.source,
- customer: payload.customerId,
- });
- },
-});
- `,
- highlight: [
- [5, 7],
- [12, 17],
- ],
- },
- {
- title: "Using Stripe triggers",
- code: `
-client.defineJob({
- id: "stripe-on-subscription-created",
- name: "Stripe On Subscription Created",
- version: "0.1.0",
- trigger: stripe.onCustomerSubscriptionCreated({
- filter: {
- currency: ["usd"],
- },
- }),
- run: async (payload, io, ctx) => {
- await io.logger.info("Subscription created in USD!");
- },
-});
- `,
- highlight: [[5, 9]],
- },
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/supabase.ts b/apps/webapp/app/services/externalApis/integrations/supabase.ts
deleted file mode 100644
index b0036aaf6b..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/supabase.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import type { HelpSample, Integration } from "../types";
-
-const managementUsageSample: HelpSample = {
- title: "Using the client",
- code: `
-import { SupabaseManagement } from "@trigger.dev/supabase";
-
-const supabase = new SupabaseManagement({
- id: "__SLUG__",
-});
-
-client.defineJob({
- id: "on-new-todos",
- name: "On New Todos",
- version: "0.1.1",
- trigger: supabase.onInserted({
- table: "todos",
- }),
- run: async (payload, io, ctx) => {
- },
-});
- `,
-};
-
-const managementApiKeyUsageSample: HelpSample = {
- title: "Using the client",
- code: `
-import { SupabaseManagement } from "@trigger.dev/supabase";
-
-const supabase = new SupabaseManagement({
- id: "__SLUG__",
- apiKey: process.env.SUPABASE_API_KEY!,
-});
-
-client.defineJob({
- id: "on-new-todos",
- name: "On New Todos",
- version: "0.1.1",
- trigger: supabase.onInserted({
- table: "todos",
- }),
- run: async (payload, io, ctx) => {
- },
-});
- `,
-};
-
-export const supabaseManagement: Integration = {
- identifier: "supabase-management",
- icon: "supabase",
- name: "Supabase Management",
- packageName: "@trigger.dev/supabase",
- description: "Use database webhooks, manage your organizations and projects.",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { SupabaseManagement } from "@trigger.dev/supabase";
-
-const supabase = new SupabaseManagement({
- id: "__SLUG__"
- apiKey: process.env.SUPABASE_API_KEY!,
-});
-`,
- },
- managementApiKeyUsageSample,
- ],
- },
- },
- oauth2: {
- name: "OAuth",
- type: "oauth2",
- client: {
- id: {
- envName: "CLOUD_SUPABASE_CLIENT_ID",
- },
- secret: {
- envName: "CLOUD_SUPABASE_CLIENT_SECRET",
- },
- },
- config: {
- authorization: {
- url: "https://api.supabase.com/v1/oauth/authorize",
- scopeSeparator: " ",
- },
- token: {
- url: "https://api.supabase.com/v1/oauth/token",
- metadata: { accountPointer: "/team/name" },
- authorizationMethod: "body",
- },
- refresh: {
- url: "https://api.supabase.com/v1/oauth/token",
- skipScopes: true,
- },
- },
- scopes: [
- {
- name: "all",
- description:
- "Grants full access to all resources available in the Supabase Management API.",
- defaultChecked: true,
- },
- ],
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { SupabaseManagement } from "@trigger.dev/supabase";
-
-const supabase = new SupabaseManagement({
- id: "__SLUG__"
-});
-`,
- },
- managementUsageSample,
- ],
- },
- },
- },
-};
-
-const supabaseUsageSample: HelpSample = {
- title: "Using the client",
- code: `
-import { Supabase } from "@trigger.dev/supabase";
-import { Database } from "@/supabase.types";
-
-const supabase = new Supabase({
- id: "__SLUG__",
- projectId: process.env.SUPABASE_ID!,
- supabaseKey: process.env.SUPABASE_API_KEY!,
-});
-
-client.defineJob({
- id: "on-new-users",
- name: "On New Users",
- version: "0.1.1",
- trigger: eventTrigger({
- name: "foo.bar
- }),
- integrations: {
- supabase
- },
- run: async (payload, io, ctx) => {
- await io.supabase.runTask("get-users", async (db) => {
- return await db.from("users").select("*");
- });
- },
-});
- `,
-};
-
-export const supabase: Integration = {
- identifier: "supabase",
- icon: "supabase",
- name: "Supabase",
- packageName: "@trigger.dev/supabase",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Supabase } from "@trigger.dev/supabase";
-
-const supabase = new Supabase({
- id: "__SLUG__"
- projectId: process.env.SUPABASE_ID!,
- supabaseKey: process.env.SUPABASE_KEY!,
-});
-`,
- },
- ],
- },
- },
- },
-};
diff --git a/apps/webapp/app/services/externalApis/integrations/typeform.ts b/apps/webapp/app/services/externalApis/integrations/typeform.ts
deleted file mode 100644
index e57b79b35d..0000000000
--- a/apps/webapp/app/services/externalApis/integrations/typeform.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { Integration } from "../types";
-
-export const typeform: Integration = {
- identifier: "typeform",
- name: "Typeform",
- description: "Use the Typeform API and trigger on new responses",
- packageName: "@trigger.dev/typeform@latest",
- authenticationMethods: {
- apikey: {
- type: "apikey",
- help: {
- samples: [
- {
- title: "Creating the client",
- code: `
-import { Typeform } from "@trigger.dev/typeform";
-
-const typeform = new Typeform({
- id: "__SLUG__",
- apiKey: process.env.TYPEFORM_TOKEN!,
-});
-`,
- },
- {
- title: "Using the client",
- code: `
-client.defineJob({
- id: "typeform-response",
- name: "Typeform Response",
- version: "0.0.1",
- trigger: typeform.onFormResponse({
- uid: "