Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f888a49555 | |||
| 421c249e50 | |||
| a8a6f51387 | |||
| 12e73eef22 | |||
| 2e33fcb16b | |||
| 5912cdd11c | |||
| cc016b3ae3 | |||
| 7760e09462 | |||
| 3ca4456c88 | |||
| 618b7f22da | |||
| a12c7c3b0a | |||
| a42e94c75f | |||
| bc757c8ddb | |||
| 6e11ab9183 | |||
| 2397fcb640 | |||
| 35d0c2a06f | |||
| 813ec74672 | |||
| 03db13171a | |||
| 0ecb5129e8 | |||
| 44cb28c1c4 | |||
| 4578f6bd64 | |||
| 8b25e57613 | |||
| 8fb9ea19a3 | |||
| eb4ca0ce2d | |||
| df24cd5b71 | |||
| 284324031c | |||
| 2fdf42d444 | |||
| 50137a6f24 | |||
| 3703f989fa | |||
| 2a42942b01 | |||
| 7b29a92f6a | |||
| 24465c9a7b | |||
| 42022b6ba4 | |||
| dcc807d716 | |||
| b5e37bfa61 | |||
| 5681ebc756 | |||
| 886d6fda36 | |||
| 1a4952720f | |||
| c0dfa8048a | |||
| 3e63a7e7a0 | |||
| 4cc690a6ce | |||
| 537447e318 | |||
| 15f17d27e0 | |||
| 91fc1e80f3 | |||
| dfe680a390 | |||
| f73a2a424b | |||
| 6ce87f169a | |||
| ad14983e21 | |||
| 4edc7112ab | |||
| c2ce707f3d | |||
| cd70a29970 | |||
| cd94d8fee2 | |||
| 241e40e3e2 | |||
| 486ed20ae7 | |||
| e5ffc3a3f3 | |||
| 363c74c6da | |||
| f98c425186 | |||
| da10ba907f | |||
| 5aeef1233b | |||
| fb5f4e308f | |||
| 6ccb4fbc63 | |||
| 88ebf4a1a0 | |||
| 9a0e6412e9 | |||
| a68a912d56 | |||
| dde51d6d25 | |||
| dbd094216d | |||
| dc7f335beb | |||
| 454cad69b4 | |||
| f86117f92b | |||
| b4c67984bc | |||
| 1b4db79885 | |||
| e1e60499b2 | |||
| 4eb2439d64 | |||
| 2895ad52fd |
@@ -1,11 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
|
||||
"changelog": [
|
||||
"@remix-run/changelog-github",
|
||||
{
|
||||
"repo": "triggerdotdev/trigger.dev"
|
||||
}
|
||||
],
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [
|
||||
[
|
||||
|
||||
+13
-4
@@ -1,35 +1,43 @@
|
||||
*.log
|
||||
\*.log
|
||||
.git
|
||||
.github
|
||||
|
||||
# editor
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# dependencies
|
||||
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
|
||||
.next/
|
||||
build
|
||||
|
||||
# packages
|
||||
|
||||
build
|
||||
dist
|
||||
packages/**/dist
|
||||
packages/\*\*/dist
|
||||
|
||||
# misc
|
||||
|
||||
.DS_Store
|
||||
*.pem
|
||||
\*.pem
|
||||
|
||||
.turbo
|
||||
.vercel
|
||||
.cache
|
||||
.output
|
||||
apps/**/public/build
|
||||
apps/\*\*/public/build
|
||||
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
@@ -38,6 +46,7 @@ apps/**/styles/tailwind.css
|
||||
packages/**/styles/tailwind.css
|
||||
|
||||
.changeset
|
||||
references
|
||||
examples
|
||||
CHANGESETS.md
|
||||
CONTRIBUTING.md
|
||||
|
||||
@@ -31,6 +31,8 @@ CLOUD_AIRTABLE_CLIENT_ID=
|
||||
CLOUD_AIRTABLE_CLIENT_SECRET=
|
||||
CLOUD_GITHUB_CLIENT_ID=
|
||||
CLOUD_GITHUB_CLIENT_SECRET=
|
||||
CLOUD_LINEAR_CLIENT_ID=
|
||||
CLOUD_LINEAR_CLIENT_SECRET=
|
||||
CLOUD_SLACK_APP_HOST=
|
||||
CLOUD_SLACK_CLIENT_ID=
|
||||
CLOUD_SLACK_CLIENT_SECRET=
|
||||
@@ -0,0 +1,38 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Create a bug report to help us improve
|
||||
title: "bug: "
|
||||
labels: ["🐞 unconfirmed bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Provide environment information
|
||||
description: |
|
||||
Run this command in your project root and paste the results:
|
||||
```bash
|
||||
npx envinfo --system --binaries
|
||||
```
|
||||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Reproduction repo
|
||||
description: If applicable, please provide a link to a reproduction repo or a Stackblitz / CodeSandbox project. Your issue may be closed if this is not provided and we are unable to reproduce the issue. If your bug is a docs issue, link the appropriate page.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://trigger.dev/discord
|
||||
about: Ask questions and discuss with other community members
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "feat: "
|
||||
labels: ["🌟 enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternate solutions
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
||||
@@ -0,0 +1,12 @@
|
||||
"📌 area: cli":
|
||||
- any: ["cli/**/*"]
|
||||
|
||||
"📌 area: t3-app":
|
||||
- any: ["cli/template/**/*"]
|
||||
|
||||
"📚 documentation":
|
||||
- any: ["www/**/*"]
|
||||
- any: ["**/*.md"]
|
||||
|
||||
"📌 area: ci":
|
||||
- any: [".github/**/*"]
|
||||
@@ -0,0 +1,27 @@
|
||||
Closes #<issue>
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
|
||||
- [ ] The PR title follows the convention.
|
||||
- [ ] I ran and tested the code works
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
_[Describe the steps you took to test this change]_
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
_[Short description of what has changed]_
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
_[Screenshots]_
|
||||
|
||||
💯
|
||||
@@ -23,6 +23,7 @@ on:
|
||||
- "turbo.json"
|
||||
- "docker/Dockerfile"
|
||||
- "docker/scripts/**"
|
||||
- "tests/**"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -128,10 +129,10 @@ jobs:
|
||||
run: |
|
||||
# Setup environment variables
|
||||
cp ./.env.example ./.env
|
||||
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @examples/nextjs-test^...
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
|
||||
Vendored
+9
@@ -19,6 +19,15 @@
|
||||
"name": "Chrome webapp",
|
||||
"url": "http://localhost:3030",
|
||||
"webRoot": "${workspaceFolder}/apps/webapp/app"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug BYO Auth",
|
||||
"command": "pnpm run byo-auth",
|
||||
"envFile": "${workspaceFolder}/references/job-catalog/.env",
|
||||
"cwd": "${workspaceFolder}/references/job-catalog",
|
||||
"sourceMaps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,3 +27,12 @@ Please follow the best-practice of adding changesets in the same commit as the c
|
||||
3. Create version `pnpm run changeset:version`
|
||||
4. Release `pnpm run changeset:release`
|
||||
5. Switch back to normal mode by running `pnpm run changeset:normal`
|
||||
|
||||
## Snapshot instructions
|
||||
|
||||
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
|
||||
|
||||
1. Add changesets as usual `pnpm run changeset:add`
|
||||
2. Create a snapshot version (replace "dev" with your tag) `pnpm exec changeset version --snapshot dev`
|
||||
3. Build the packages: `pnpm run build --filter "@trigger.dev/*"`
|
||||
4. Publish the snapshot (replace "dev" with your tag) `pnpm exec changeset publish --no-git-tag --snapshot --tag dev`
|
||||
|
||||
+21
-13
@@ -23,11 +23,11 @@ branch are tagged into a release monthly.
|
||||
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
|
||||
|
||||
```
|
||||
git clone https://github.com/triggerdotdev/trigger.dev.git
|
||||
git clone https://github.com/<github_username>/trigger.dev.git
|
||||
```
|
||||
|
||||
> If you are on windows, run the following command on gitbash with admin privileges:
|
||||
> `git clone -c core.symlinks=true https://github.com/triggerdotdev/trigger.dev.git`
|
||||
> `git clone -c core.symlinks=true https://github.com/<github_username>/trigger.dev.git`
|
||||
|
||||
2. Navigate to the project folder
|
||||
```
|
||||
@@ -133,10 +133,10 @@ pnpm run dev
|
||||
|
||||
2. Open a new Terminal window and run the webapp locally and then create a new project in the dashboard. Copy out the dev API key.
|
||||
|
||||
3. Create a new temporary Next.js app in examples directory
|
||||
3. Create a new temporary Next.js app in references directory
|
||||
|
||||
```sh
|
||||
cd ./examples
|
||||
cd ./references
|
||||
pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir --import-alias "@/*"
|
||||
```
|
||||
|
||||
@@ -149,7 +149,7 @@ pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir
|
||||
}
|
||||
```
|
||||
|
||||
5. Back in the terminal, navigate into the example, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
|
||||
5. Back in the terminal, navigate into the reference, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
|
||||
|
||||
```sh
|
||||
cd ./test-cli
|
||||
@@ -179,14 +179,14 @@ To run the end-to-end tests, follow the steps below:
|
||||
|
||||
```sh
|
||||
cp ./.env.example ./.env
|
||||
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
```
|
||||
|
||||
2. Set up dependencies
|
||||
|
||||
```sh
|
||||
# Build packages
|
||||
pnpm run build --filter @examples/nextjs-test^...
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
@@ -221,11 +221,11 @@ pnpm run db:studio
|
||||
|
||||
## Add sample jobs
|
||||
|
||||
The [examples/jobs-starter](./examples/jobs-starter/) project defines simple jobs you can get started with.
|
||||
The [references/job-catalog](./references/job-catalog/) project defines simple jobs you can get started with.
|
||||
|
||||
1. `cd` into `examples/jobs-starter`
|
||||
2. Create a `.env.local` file with the following content,
|
||||
replacing `[TRIGGER_DEV_API_KEY]` with an actual key:
|
||||
1. `cd` into `references/job-catalog`
|
||||
2. Create a `.env` file with the following content,
|
||||
replacing `<TRIGGER_DEV_API_KEY>` with an actual key:
|
||||
|
||||
```env
|
||||
TRIGGER_API_KEY=[TRIGGER_DEV_API_KEY]
|
||||
@@ -235,12 +235,20 @@ TRIGGER_API_URL=http://localhost:3030
|
||||
`TRIGGER_API_URL` is used to configure the URL for your Trigger.dev instance,
|
||||
where the jobs will be registered.
|
||||
|
||||
3. Run the `jobs-starter` app:
|
||||
3. Run one of the the `job-catalog` files:
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
pnpm run events
|
||||
```
|
||||
|
||||
This will open up a local server using `express` on port 8080. Then in a new terminal window you can run the trigger-cli dev command:
|
||||
|
||||
```sh
|
||||
pnpm run dev:trigger
|
||||
```
|
||||
|
||||
See the [Job Catalog](./references/job-catalog/README.md) file for more.
|
||||
|
||||
4. Navigate to your trigger.dev instance ([http://localhost:3030](http://localhost:3030/)), to see the jobs.
|
||||
You can use the test feature to trigger them.
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export function TriggerDevStep() {
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
<Paragraph spacing variant="small">
|
||||
If you’re not running on port 3000 you can specify the port by adding{" "}
|
||||
If you’re not running on the default you can specify the port by adding{" "}
|
||||
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
|
||||
@@ -51,18 +51,18 @@ export function FrameworkSelector() {
|
||||
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
|
||||
<NextjsLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupExpressPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupExpressPath(organization, project)} supported>
|
||||
<ExpressLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
|
||||
<RemixLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
|
||||
<AstroLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupAstroPath(organization, project)}>
|
||||
<AstroLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
|
||||
<NuxtLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
|
||||
@@ -272,6 +272,21 @@ export function HowToUseApiKeysAndEndpoints() {
|
||||
you should use the Test feature to trigger any scheduled Jobs.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Staging</span>
|
||||
<EnvironmentLabel environment={{ type: "STAGING" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
The <InlineCode>STAGING</InlineCode> environment is where your Jobs will run in a staging
|
||||
environment, meant to mirror your production environment.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Client } from "~/presenters/IntegrationsPresenter.server";
|
||||
import { UpdateOAuthForm } from "./UpdateOAuthForm";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
|
||||
export function SelectOAuthMethod({
|
||||
integration,
|
||||
@@ -76,7 +77,7 @@ export function SelectOAuthMethod({
|
||||
id="EXTERNAL"
|
||||
value="EXTERNAL"
|
||||
label="Your users"
|
||||
description="We will give you OAuth React components so you can connect as your users."
|
||||
description="Use an external authentication provider or your own user database to provide auth credentails of your users."
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
@@ -108,14 +109,19 @@ export function SelectOAuthMethod({
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Header2 className="mb-1 mt-4">User OAuth coming soon</Header2>
|
||||
<Header2 className="mb-1 mt-4">BYO Auth</Header2>
|
||||
<Paragraph spacing>
|
||||
End-user OAuth is going to be released soon. If you are interested in being an early
|
||||
beta tester then please{" "}
|
||||
<a href="mailto:founders@trigger.dev" className="text-indigo-500 underline">
|
||||
message us
|
||||
</a>
|
||||
.
|
||||
We support external authentication providers through Auth Resolvers. Read the docs to
|
||||
learn more:{" "}
|
||||
<LinkButton
|
||||
variant="secondary/small"
|
||||
LeadingIcon={"docs"}
|
||||
TrailingIcon={"external-link"}
|
||||
to="https://trigger.dev/docs/documentation/guides/using-integrations-byo-auth"
|
||||
target="_blank"
|
||||
>
|
||||
Bring your own Auth
|
||||
</LinkButton>
|
||||
</Paragraph>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -197,6 +197,8 @@ function classForJobStatus(status: JobRunStatus) {
|
||||
case "TIMED_OUT":
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
case "PENDING":
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "INVALID_PAYLOAD":
|
||||
return "text-rose-500";
|
||||
default:
|
||||
return "";
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
organizationTeamPath,
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectSetupPath,
|
||||
projectPath,
|
||||
projectSetupPath,
|
||||
projectTriggersPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
@@ -96,7 +96,7 @@ export function ProjectSideMenu() {
|
||||
data-action="environments & api keys"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<SideMenuItem
|
||||
name="Team"
|
||||
icon="team"
|
||||
@@ -118,6 +118,14 @@ export function ProjectSideMenu() {
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="onboarding"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="list"
|
||||
to="https://trigger.dev/changelog"
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Account"
|
||||
icon={UserProfilePhoto}
|
||||
@@ -146,6 +154,7 @@ function SideMenuItem({
|
||||
isCollapsed,
|
||||
forceActive,
|
||||
hasWarning = false,
|
||||
target,
|
||||
}: {
|
||||
icon: IconNames | React.ComponentType<any>;
|
||||
name: string;
|
||||
@@ -153,6 +162,7 @@ function SideMenuItem({
|
||||
isCollapsed: boolean;
|
||||
hasWarning?: boolean;
|
||||
forceActive?: boolean;
|
||||
target?: string;
|
||||
}) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
@@ -164,13 +174,14 @@ function SideMenuItem({
|
||||
LeadingIcon={icon}
|
||||
leadingIconClassName="text-dimmed"
|
||||
to={to}
|
||||
target={target}
|
||||
className={({ isActive, isPending }) => {
|
||||
if (forceActive !== undefined) {
|
||||
isActive = forceActive;
|
||||
}
|
||||
return cn(
|
||||
"relative",
|
||||
isActive
|
||||
isActive || isPending
|
||||
? "bg-slate-800 text-bright group-hover:bg-slate-800"
|
||||
: "text-dimmed group-hover:bg-slate-850 group-hover:text-bright"
|
||||
);
|
||||
|
||||
@@ -286,9 +286,13 @@ type NavLinkPropsType = Pick<NavLinkProps, "to" | "target"> &
|
||||
Omit<React.ComponentProps<typeof ButtonContent>, "className"> & {
|
||||
className?: (props: { isActive: boolean; isPending: boolean }) => string | undefined;
|
||||
};
|
||||
export const NavLinkButton = ({ to, className, ...props }: NavLinkPropsType) => {
|
||||
export const NavLinkButton = ({ to, className, target, ...props }: NavLinkPropsType) => {
|
||||
return (
|
||||
<NavLink to={to} className={cn("group outline-none", props.fullWidth ? "w-full" : "")}>
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
target={target}
|
||||
>
|
||||
{({ isActive, isPending }) => (
|
||||
<ButtonContent className={className && className({ isActive, isPending })} {...props} />
|
||||
)}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
GlobeAltIcon,
|
||||
HandRaisedIcon,
|
||||
HeartIcon,
|
||||
HomeIcon,
|
||||
KeyIcon,
|
||||
LightBulbIcon,
|
||||
ListBulletIcon,
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
UserGroupIcon,
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
WindowIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
@@ -74,7 +76,9 @@ const icons = {
|
||||
"arrow-left": (className: string) => <ArrowLeftIcon className={cn("text-white", className)} />,
|
||||
background: (className: string) => <CloudIcon className={cn("text-sky-400", className)} />,
|
||||
beaker: (className: string) => <BeakerIcon className={cn("text-purple-500", className)} />,
|
||||
bell: (className: string) => <BellAlertIcon className={cn("text-amber-500", className)} />,
|
||||
billing: (className: string) => <CreditCardIcon className={cn("text-teal-500", className)} />,
|
||||
browser: (className: string) => <WindowIcon className={cn("text-dimmed", className)} />,
|
||||
calendar: (className: string) => (
|
||||
<CalendarDaysIcon className={cn("text-purple-500", className)} />
|
||||
),
|
||||
@@ -111,6 +115,7 @@ const icons = {
|
||||
<HandRaisedIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
heart: (className: string) => <HeartIcon className={cn("text-rose-500", className)} />,
|
||||
house: (className: string) => <HomeIcon className={cn("text-dimmed", className)} />,
|
||||
id: (className: string) => <FingerPrintIcon className={cn("text-rose-200", className)} />,
|
||||
inactive: (className: string) => <XCircleIcon className={cn("text-rose-500", className)} />,
|
||||
info: (className: string) => <InformationCircleIcon className={cn("text-blue-500", className)} />,
|
||||
@@ -126,6 +131,7 @@ const icons = {
|
||||
"clipboard-checked": (className: string) => (
|
||||
<ClipboardDocumentCheckIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
list: (className: string) => <ListBulletIcon className={cn("text-slate-400", className)} />,
|
||||
log: (className: string) => (
|
||||
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ 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, useRun } from "~/hooks/useRun";
|
||||
import { MatchedRun } from "~/hooks/useRun";
|
||||
import { formatDuration } from "~/utils";
|
||||
import {
|
||||
RunPanel,
|
||||
|
||||
@@ -167,7 +167,13 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
|
||||
<RunPanelHeader icon={trigger.icon} title={trigger.title} />
|
||||
<RunPanelBody>
|
||||
<RunPanelProperties
|
||||
properties={[{ label: "Event name", text: run.event.name }, ...run.properties]}
|
||||
properties={[{ label: "Event name", text: run.event.name }]
|
||||
.concat(
|
||||
run.event.externalAccount
|
||||
? [{ label: "Account ID", text: run.event.externalAccount.identifier }]
|
||||
: []
|
||||
)
|
||||
.concat(run.properties)}
|
||||
/>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
|
||||
@@ -45,6 +45,13 @@ export function TriggerDetail({
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
{trigger.externalAccount && (
|
||||
<RunPanelIconProperty
|
||||
icon="account"
|
||||
label="Account ID"
|
||||
value={trigger.externalAccount.identifier}
|
||||
/>
|
||||
)}
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { JobRunExecution, JobRunStatus } from "@trigger.dev/database";
|
||||
import { NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
ExclamationTriangleIcon,
|
||||
StopIcon,
|
||||
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 { HandRaisedIcon, NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export function hasFinished(status: JobRunStatus): boolean {
|
||||
return (
|
||||
@@ -17,7 +16,9 @@ export function hasFinished(status: JobRunStatus): boolean {
|
||||
status === "FAILURE" ||
|
||||
status === "ABORTED" ||
|
||||
status === "TIMED_OUT" ||
|
||||
status === "CANCELED"
|
||||
status === "CANCELED" ||
|
||||
status === "UNRESOLVED_AUTH" ||
|
||||
status === "INVALID_PAYLOAD"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +49,9 @@ export function RunStatusIcon({ status, className }: { status: JobRunStatus; cla
|
||||
return <XCircleIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "TIMED_OUT":
|
||||
return <ExclamationTriangleIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "INVALID_PAYLOAD":
|
||||
return <XCircleIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
return <WrenchIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "ABORTED":
|
||||
@@ -63,26 +67,26 @@ export type RunBasicStatus = "WAITING" | "PENDING" | "RUNNING" | "COMPLETED" | "
|
||||
|
||||
export function runBasicStatus(status: JobRunStatus): RunBasicStatus {
|
||||
switch (status) {
|
||||
case "SUCCESS":
|
||||
return "COMPLETED";
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
case "QUEUED":
|
||||
case "PREPROCESSING":
|
||||
case "PENDING":
|
||||
return "PENDING";
|
||||
case "STARTED":
|
||||
return "RUNNING";
|
||||
case "QUEUED":
|
||||
return "PENDING";
|
||||
case "FAILURE":
|
||||
return "FAILED";
|
||||
case "TIMED_OUT":
|
||||
return "FAILED";
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
return "PENDING";
|
||||
case "ABORTED":
|
||||
return "FAILED";
|
||||
case "PREPROCESSING":
|
||||
return "PENDING";
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "CANCELED":
|
||||
case "ABORTED":
|
||||
case "INVALID_PAYLOAD":
|
||||
return "FAILED";
|
||||
case "SUCCESS":
|
||||
return "COMPLETED";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +112,14 @@ export function runStatusTitle(status: JobRunStatus): string {
|
||||
return "Preprocessing";
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
case "UNRESOLVED_AUTH":
|
||||
return "Unresolved auth";
|
||||
case "INVALID_PAYLOAD":
|
||||
return "Invalid payload";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +134,8 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
case "QUEUED":
|
||||
return "text-amber-300";
|
||||
case "FAILURE":
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "INVALID_PAYLOAD":
|
||||
return "text-rose-500";
|
||||
case "TIMED_OUT":
|
||||
return "text-amber-300";
|
||||
|
||||
@@ -31,7 +31,7 @@ export type PrismaTransactionOptions = {
|
||||
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||
|
||||
rethrowPrismaErrors?: boolean;
|
||||
swallowPrismaErrors?: boolean;
|
||||
};
|
||||
|
||||
export async function $transaction<R>(
|
||||
@@ -55,11 +55,9 @@ export async function $transaction<R>(
|
||||
name: error.name,
|
||||
});
|
||||
|
||||
if (options?.rethrowPrismaErrors) {
|
||||
throw error;
|
||||
if (options?.swallowPrismaErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -124,6 +122,10 @@ function getClient() {
|
||||
emit: "stdout",
|
||||
level: "warn",
|
||||
},
|
||||
// {
|
||||
// emit: "stdout",
|
||||
// level: "query",
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
import { customAlphabet } from "nanoid";
|
||||
import slug from "slug";
|
||||
import { prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { createProject } from "./project.server";
|
||||
|
||||
export type { Organization };
|
||||
@@ -76,6 +75,10 @@ export async function createOrganization(
|
||||
},
|
||||
attemptCount = 0
|
||||
): Promise<Organization & { projects: Project[] }> {
|
||||
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
|
||||
throw new Error("Organization could not be created.");
|
||||
}
|
||||
|
||||
const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`;
|
||||
|
||||
const orgWithSameSlug = await prisma.organization.findFirst({
|
||||
@@ -172,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
return "prod";
|
||||
}
|
||||
case "STAGING": {
|
||||
return "staging";
|
||||
return "stg";
|
||||
}
|
||||
case "PREVIEW": {
|
||||
return "preview";
|
||||
return "prev";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function createProject(
|
||||
|
||||
// Create the dev and prod environments
|
||||
await createEnvironment(organization, project, "PRODUCTION");
|
||||
await createEnvironment(organization, project, "STAGING");
|
||||
|
||||
for (const member of project.organization.members) {
|
||||
await createEnvironment(organization, project, "DEVELOPMENT", member);
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function resolveRunConnections(
|
||||
const result: Record<string, ConnectionAuth> = {};
|
||||
|
||||
for (const connection of connections) {
|
||||
if (connection.integration.authSource === "LOCAL") {
|
||||
if (connection.integration.authSource !== "HOSTED") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@ import { PrismaClient, prisma } from "~/db.server";
|
||||
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import {
|
||||
import type {
|
||||
Endpoint,
|
||||
EndpointIndex,
|
||||
RuntimeEnvironment,
|
||||
RuntimeEnvironmentType,
|
||||
} from "../../../../packages/database/src";
|
||||
import { env } from "~/env.server";
|
||||
} from "@trigger.dev/database";
|
||||
|
||||
export type Client = {
|
||||
slug: string;
|
||||
endpoints: {
|
||||
DEVELOPMENT: ClientEndpoint;
|
||||
PRODUCTION: ClientEndpoint;
|
||||
STAGING?: ClientEndpoint;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -133,6 +133,8 @@ export class EnvironmentsPresenter {
|
||||
throw new Error("Development environment not found, this should not happen");
|
||||
}
|
||||
|
||||
const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING");
|
||||
|
||||
const productionEnvironment = filtered.find(
|
||||
(environment) => environment.type === "PRODUCTION"
|
||||
);
|
||||
@@ -151,6 +153,9 @@ export class EnvironmentsPresenter {
|
||||
state: "unconfigured",
|
||||
environment: productionEnvironment,
|
||||
},
|
||||
STAGING: stagingEnvironment
|
||||
? { state: "unconfigured", environment: stagingEnvironment }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -161,6 +166,16 @@ export class EnvironmentsPresenter {
|
||||
client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl);
|
||||
}
|
||||
|
||||
if (stagingEnvironment) {
|
||||
const stagingEndpoint = stagingEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
if (stagingEndpoint) {
|
||||
client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const prodEndpoint = productionEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
@@ -120,8 +120,12 @@ export class IntegrationClientPresenter {
|
||||
icon: integration.definition.icon,
|
||||
},
|
||||
authMethod: {
|
||||
type: integration.authMethod?.type ?? "local",
|
||||
name: integration.authMethod?.name ?? "Local Auth",
|
||||
type:
|
||||
integration.authMethod?.type ?? integration.authSource === "RESOLVER" ? "local" : "local",
|
||||
name:
|
||||
integration.authMethod?.name ?? integration.authSource === "RESOLVER"
|
||||
? "Auth Resolver"
|
||||
: "Local Auth",
|
||||
},
|
||||
help,
|
||||
};
|
||||
|
||||
@@ -125,8 +125,9 @@ export class IntegrationsPresenter {
|
||||
name: c.definition.name,
|
||||
},
|
||||
authMethod: {
|
||||
type: c.authMethod?.type ?? "local",
|
||||
name: c.authMethod?.name ?? "Local Only",
|
||||
type: c.authMethod?.type ?? c.authSource === "RESOLVER" ? "resolver" : "local",
|
||||
name:
|
||||
c.authMethod?.name ?? c.authSource === "RESOLVER" ? "Auth Resolver" : "Local Only",
|
||||
},
|
||||
authSource: c.authSource,
|
||||
setupStatus: c.setupStatus,
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
export class OrgUsagePresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({ userId, slug }: { userId: string; slug: string }) {
|
||||
const organization = await this.#prismaClient.organization.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
||||
const startOfLastMonth = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1); // this works for January as well
|
||||
|
||||
// Get count of runs since the start of the current month
|
||||
const runsCount = await this.#prismaClient.jobRun.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get the count of runs for last month
|
||||
const runsCountLastMonth = await this.#prismaClient.jobRun.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
gte: startOfLastMonth,
|
||||
lt: startOfMonth,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be:
|
||||
// [
|
||||
// { month: "2021-01", count: 10 },
|
||||
// { month: "2021-02", count: 20 },
|
||||
// { month: "2021-03", count: 30 },
|
||||
// { month: "2021-04", count: 40 },
|
||||
// { month: "2021-05", count: 50 },
|
||||
// { month: "2021-06", count: 60 },
|
||||
// ]
|
||||
// This will be used to generate the chart on the usage page
|
||||
// Use prisma queryRaw for this since prisma doesn't support grouping by month
|
||||
const chartDataRaw = await this.#prismaClient.$queryRaw<
|
||||
{
|
||||
month: string;
|
||||
count: number;
|
||||
}[]
|
||||
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`;
|
||||
|
||||
const chartData = chartDataRaw.map((obj) => ({
|
||||
name: obj.month,
|
||||
total: Number(obj.count), // Convert BigInt to Number
|
||||
}));
|
||||
|
||||
const totalJobs = await this.#prismaClient.job.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
const totalJobsLastMonth = await this.#prismaClient.job.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
lt: startOfMonth,
|
||||
},
|
||||
deletedAt: null,
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
const totalIntegrations = await this.#prismaClient.integration.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const totalIntegrationsLastMonth = await this.#prismaClient.integration.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
lt: startOfMonth,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalMembers = await this.#prismaClient.orgMember.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const jobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
deletedAt: null,
|
||||
internal: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
_count: {
|
||||
select: {
|
||||
runs: {
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startOfMonth,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: organization.id,
|
||||
runsCount,
|
||||
runsCountLastMonth,
|
||||
chartData: fillInMissingMonthlyData(chartData, 6),
|
||||
totalJobs,
|
||||
totalJobsLastMonth,
|
||||
totalIntegrations,
|
||||
totalIntegrationsLastMonth,
|
||||
totalMembers,
|
||||
jobs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// This will fill in missing chart data with zeros
|
||||
// So for example, if data is [{ name: "2021-01", total: 10 }, { name: "2021-03", total: 30 }] and the totalNumberOfMonths is 6
|
||||
// And the current month is "2021-04", then this function will return:
|
||||
// [{ name: "2020-11", total: 0 }, { name: "2020-12", total: 0 }, { name: "2021-01", total: 10 }, { name: "2021-02", total: 0 }, { name: "2021-03", total: 30 }, { name: "2021-04", total: 0 }]
|
||||
function fillInMissingMonthlyData(
|
||||
data: Array<{ name: string; total: number }>,
|
||||
totalNumberOfMonths: number
|
||||
): Array<{ name: string; total: number }> {
|
||||
const currentMonth = new Date().toISOString().slice(0, 7);
|
||||
|
||||
const startMonth = new Date(
|
||||
new Date(currentMonth).getFullYear(),
|
||||
new Date(currentMonth).getMonth() - totalNumberOfMonths,
|
||||
1
|
||||
)
|
||||
.toISOString()
|
||||
.slice(0, 7);
|
||||
|
||||
const months = getMonthsBetween(startMonth, currentMonth);
|
||||
|
||||
let completeData = months.map((month) => {
|
||||
let foundData = data.find((d) => d.name === month);
|
||||
return foundData ? { ...foundData } : { name: month, total: 0 };
|
||||
});
|
||||
|
||||
return completeData;
|
||||
}
|
||||
|
||||
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
|
||||
const startDate = new Date(startMonth);
|
||||
const endDate = new Date(endMonth);
|
||||
|
||||
const months = [];
|
||||
let currentDate = startDate;
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
months.push(currentDate.toISOString().slice(0, 7));
|
||||
currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
|
||||
}
|
||||
|
||||
return months;
|
||||
}
|
||||
@@ -115,6 +115,11 @@ export class RunPresenter {
|
||||
payload: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
select: {
|
||||
identifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RedactSchema } from "@trigger.dev/core";
|
||||
import { StyleSchema } from "@trigger.dev/core";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
import { Redactor } from "~/utils/redactor";
|
||||
|
||||
type DetailsProps = {
|
||||
id: string;
|
||||
@@ -61,6 +63,7 @@ export class TaskDetailsPresenter {
|
||||
completedAt: true,
|
||||
style: true,
|
||||
parentId: true,
|
||||
redact: true,
|
||||
attempts: {
|
||||
select: {
|
||||
number: true,
|
||||
@@ -85,11 +88,32 @@ export class TaskDetailsPresenter {
|
||||
|
||||
return {
|
||||
...task,
|
||||
output: task.output ? JSON.stringify(task.output, null, 2) : undefined,
|
||||
redact: undefined,
|
||||
output: task.output
|
||||
? JSON.stringify(this.#stringifyOutputWithRedactions(task.output, task.redact), null, 2)
|
||||
: undefined,
|
||||
connection: task.runConnection,
|
||||
params: task.params as Record<string, any>,
|
||||
properties: mergeProperties(task.properties, task.outputProperties),
|
||||
style: task.style ? StyleSchema.parse(task.style) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
#stringifyOutputWithRedactions(output: any, redact: unknown): any {
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedRedact = RedactSchema.safeParse(redact);
|
||||
|
||||
if (!parsedRedact.success) {
|
||||
return output;
|
||||
}
|
||||
|
||||
const paths = parsedRedact.data.paths;
|
||||
|
||||
const redactor = new Redactor(paths);
|
||||
|
||||
return redactor.redact(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,15 @@ export class TestJobPresenter {
|
||||
payload: true,
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
select: {
|
||||
integration: {
|
||||
select: {
|
||||
authSource: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
@@ -99,6 +108,9 @@ export class TestJobPresenter {
|
||||
...example,
|
||||
payload: JSON.stringify(example.payload, exampleReplacer, 2),
|
||||
})),
|
||||
hasAuthResolver: alias.version.integrations.some(
|
||||
(i) => i.integration.authSource === "RESOLVER"
|
||||
),
|
||||
})),
|
||||
hasTestRuns: job._count.runs > 0,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,11 @@ export class TriggerDetailsPresenter {
|
||||
payload: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
select: {
|
||||
identifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,17 +1,178 @@
|
||||
import { ComingSoon } from "~/components/ComingSoon";
|
||||
import { PageContainer, PageBody } from "~/components/layout/AppLayout";
|
||||
import { ArrowRightIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
ForwardIcon,
|
||||
SquaresPlusIcon,
|
||||
UsersIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder";
|
||||
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
|
||||
import { Link } from "@remix-run/react/dist/components";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ params, request }: LoaderArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
const presenter = new OrgUsagePresenter();
|
||||
|
||||
const data = await presenter.call({ userId, slug: organizationSlug });
|
||||
|
||||
if (!data) {
|
||||
throw new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return typedjson(data);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
|
||||
if (active && payload) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded border border-border bg-slate-900 px-4 py-2 text-sm text-dimmed">
|
||||
<p className="text-white">{label}:</p>
|
||||
<p className="text-white">{payload[0].value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const loaderData = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<OrgAdminHeader />
|
||||
<PageBody>
|
||||
<ComingSoon
|
||||
title="Usage & billing"
|
||||
description="View your usage, tier and billing information. During the beta we will display usage and start billing if you exceed your limits. But don't worry, we'll give you plenty of warning."
|
||||
icon="billing"
|
||||
/>
|
||||
<div className="mb-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Total Runs this month</Header2>
|
||||
<ForwardIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.runsCount.toLocaleString()}</p>
|
||||
<Paragraph variant="small" className="text-dimmed">
|
||||
{loaderData.runsCountLastMonth} runs last month
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Total Jobs</Header2>
|
||||
<WrenchScrewdriverIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.totalJobs.toLocaleString()}</p>
|
||||
<Paragraph variant="small" className="text-dimmed">
|
||||
{loaderData.totalJobs === loaderData.totalJobsLastMonth ? (
|
||||
<>No change since last month</>
|
||||
) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? (
|
||||
<>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month</>
|
||||
) : (
|
||||
<>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month</>
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Total Integrations</Header2>
|
||||
<SquaresPlusIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.totalIntegrations.toLocaleString()}</p>
|
||||
<Paragraph variant="small" className="text-dimmed">
|
||||
{loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? (
|
||||
<>No change since last month</>
|
||||
) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? (
|
||||
<>
|
||||
+{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since
|
||||
last month
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
-{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since
|
||||
last month
|
||||
</>
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Team members</Header2>
|
||||
<UsersIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.totalMembers.toLocaleString()}</p>
|
||||
<TextLink
|
||||
to={organizationTeamPath(organization)}
|
||||
className="group text-sm text-dimmed hover:text-bright"
|
||||
>
|
||||
Manage
|
||||
<ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" />
|
||||
</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex max-h-[500px] gap-x-4">
|
||||
<div className="w-1/2 rounded border border-border py-6 pr-2">
|
||||
<Header2 className="mb-8 pl-6">Job Runs per month</Header2>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<BarChart data={loaderData.chartData}>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#888888"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#888888"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `${value}`}
|
||||
/>
|
||||
<Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} content={<CustomTooltip />} />
|
||||
<Bar dataKey="total" fill="#DB2777" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="w-1/2 overflow-y-auto rounded border border-border px-3 py-6">
|
||||
<div className="mb-2 flex items-baseline justify-between border-b border-border px-3 pb-4">
|
||||
<Header2 className="">Jobs</Header2>
|
||||
<Header2 className="">Runs</Header2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{loaderData.jobs.map((job) => (
|
||||
<Link
|
||||
to={jobPath(organization, job.project, job)}
|
||||
className="flex items-center rounded px-4 py-3 transition hover:bg-slate-850"
|
||||
key={job.id}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{job.slug}</p>
|
||||
<p className="text-sm text-muted-foreground">Project: {job.project.name}</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">{job._count.runs.toLocaleString()}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
+2
-1
@@ -104,6 +104,7 @@ export default function Page() {
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<HelpTrigger title="Example Jobs and inspiration" />
|
||||
</div>
|
||||
@@ -160,7 +161,7 @@ function ExampleJobs() {
|
||||
height="250"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
className="mb-4 w-full border-b border-slate-800"
|
||||
className="mb-4 border-b border-slate-800"
|
||||
/>
|
||||
<Header2 spacing>How to create a Job</Header2>
|
||||
<Paragraph variant="small" spacing>
|
||||
|
||||
+15
-3
@@ -85,8 +85,8 @@ export default function Page() {
|
||||
const client = clients.find((c) => c.slug === selected.client);
|
||||
if (!client) return undefined;
|
||||
|
||||
if (selected.type === "PREVIEW" || selected.type === "STAGING") {
|
||||
throw new Error("PREVIEW/STAGING is not yet supported");
|
||||
if (selected.type === "PREVIEW") {
|
||||
throw new Error("PREVIEW is not yet supported");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -195,6 +195,18 @@ export default function Page() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
{client.endpoints.STAGING && (
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.STAGING}
|
||||
type="STAGING"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "STAGING",
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.PRODUCTION}
|
||||
type="PRODUCTION"
|
||||
@@ -218,7 +230,7 @@ export default function Page() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedEndpoint && (
|
||||
{selectedEndpoint && selectedEndpoint.endpoint && (
|
||||
<ConfigureEndpointSheet
|
||||
slug={selectedEndpoint.clientSlug}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
|
||||
+36
-17
@@ -1,4 +1,4 @@
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { PopoverTrigger } from "@radix-ui/react-popover";
|
||||
import { Form, useActionData, useSubmit } from "@remix-run/react";
|
||||
@@ -14,6 +14,9 @@ import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Popover, PopoverContent } from "~/components/primitives/Popover";
|
||||
import {
|
||||
Select,
|
||||
@@ -69,6 +72,7 @@ const schema = z.object({
|
||||
}),
|
||||
environmentId: z.string(),
|
||||
versionId: z.string(),
|
||||
accountId: z.string().optional(),
|
||||
});
|
||||
|
||||
//todo save the chosen environment to a cookie (for that user), use it to default the env dropdown
|
||||
@@ -84,11 +88,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
|
||||
const testService = new TestJobService();
|
||||
const run = await testService.call({
|
||||
environmentId: submission.value.environmentId,
|
||||
payload: submission.value.payload,
|
||||
versionId: submission.value.versionId,
|
||||
});
|
||||
const run = await testService.call(submission.value);
|
||||
|
||||
if (!run) {
|
||||
return redirectBackWithErrorMessage(
|
||||
@@ -124,6 +124,7 @@ export default function Page() {
|
||||
const [defaultJson, setDefaultJson] = useState<string>(startingJson);
|
||||
const currentJson = useRef<string>(defaultJson);
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>(environments[0].id);
|
||||
const [currentAccountId, setCurrentAccountId] = useState<string | undefined>(undefined);
|
||||
|
||||
const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId);
|
||||
|
||||
@@ -139,6 +140,7 @@ export default function Page() {
|
||||
payload: currentJson.current,
|
||||
environmentId: selectedEnvironmentId,
|
||||
versionId: selectedEnvironment?.versionId ?? "",
|
||||
...(currentAccountId ? { accountId: currentAccountId } : {}),
|
||||
},
|
||||
{
|
||||
action: "",
|
||||
@@ -147,10 +149,10 @@ export default function Page() {
|
||||
);
|
||||
e.preventDefault();
|
||||
},
|
||||
[currentJson, selectedEnvironmentId]
|
||||
[currentJson, selectedEnvironmentId, currentAccountId]
|
||||
);
|
||||
|
||||
const [form, { environmentId, payload }] = useForm({
|
||||
const [form, { environmentId, payload, accountId }] = useForm({
|
||||
id: "test-job",
|
||||
lastSubmission,
|
||||
onValidate({ formData }) {
|
||||
@@ -234,15 +236,32 @@ export default function Page() {
|
||||
</div>
|
||||
<HelpTrigger title="How do I run a test?" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<JSONEditor
|
||||
defaultValue={defaultJson}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => (currentJson.current = v)}
|
||||
minHeight="150px"
|
||||
/>
|
||||
</div>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Payload</Label>
|
||||
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<JSONEditor
|
||||
defaultValue={defaultJson}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => (currentJson.current = v)}
|
||||
minHeight="150px"
|
||||
/>
|
||||
</div>
|
||||
</InputGroup>
|
||||
|
||||
{selectedEnvironment?.hasAuthResolver && (
|
||||
<InputGroup fullWidth className="mb-4 mt-4">
|
||||
<Label variant="small">Account ID</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
value={currentAccountId}
|
||||
placeholder={`e.g. abc_1234`}
|
||||
onChange={(e) => setCurrentAccountId(e.target.value)}
|
||||
/>
|
||||
<FormError>{accountId.error}</FormError>
|
||||
</InputGroup>
|
||||
)}
|
||||
<div className="flex flex-none items-center justify-between">
|
||||
{payload.error ? (
|
||||
<FormError id={payload.errorId}>{payload.error}</FormError>
|
||||
|
||||
+101
-10
@@ -1,21 +1,112 @@
|
||||
import { AstroLogo } from "~/assets/logos/AstroLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
} from "~/components/primitives/ClientTabs";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Astro" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
export default function SetUpAstro() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Astro"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/452"
|
||||
githubIssueNumber={452}
|
||||
>
|
||||
<AstroLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<AstroLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Run the CLI 'init' command in an existing Astro project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple
|
||||
example Job in <InlineCode variant="extra-small">example.ts</InlineCode> to help you
|
||||
get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Astro app" />
|
||||
<StepContentContainer>
|
||||
<RunDevCommand />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+108
-9
@@ -1,21 +1,120 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Express" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Express"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/451"
|
||||
githubIssueNumber={451}
|
||||
>
|
||||
<ExpressLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<ExpressLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Manually set up Trigger.dev in your existing Express project"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/express"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Express app" />
|
||||
<StepContentContainer>
|
||||
<RunDevCommand />
|
||||
<Callout variant="info">
|
||||
You may be using the `start` script instead, in which case substitute `dev` in the
|
||||
above commands.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+4
@@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
|
||||
|
||||
type SelectionChoices = "use-existing-project" | "create-new-next-app";
|
||||
|
||||
@@ -48,6 +49,9 @@ export default function SetupNextjs() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<NextjsLogo className="w-56" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
|
||||
|
||||
+16
-22
@@ -25,8 +25,9 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { RemixLogo } from "~/assets/logos/RemixLogo";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Remix" />,
|
||||
@@ -38,9 +39,14 @@ export default function SetUpRemix() {
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<RemixLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
@@ -75,28 +81,16 @@ export default function SetUpRemix() {
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Follow the steps from the Remix manual installation guide"
|
||||
title="Run the CLI 'init' command in an existing Remix project"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/remix"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
<div className="flex items-start justify-start gap-2"></div>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple
|
||||
example Job in <InlineCode variant="extra-small">example.server.ts</InlineCode> to
|
||||
help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Remix app" />
|
||||
<StepContentContainer>
|
||||
|
||||
@@ -81,13 +81,19 @@ class CreateExternalConnectionService {
|
||||
environment: AuthenticatedEnvironment,
|
||||
payload: CreateExternalConnectionBody
|
||||
) {
|
||||
const externalAccount = await this.#prismaClient.externalAccount.findUniqueOrThrow({
|
||||
const externalAccount = await this.#prismaClient.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountIdentifier,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountIdentifier,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const integration = await this.#prismaClient.integration.findUniqueOrThrow({
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { TaskStatus } from "@trigger.dev/database";
|
||||
import {
|
||||
RunTaskBodyOutput,
|
||||
RunTaskBodyOutputSchema,
|
||||
ServerTask,
|
||||
StatusHistory,
|
||||
StatusHistorySchema,
|
||||
StatusUpdate,
|
||||
StatusUpdateData,
|
||||
StatusUpdateSchema,
|
||||
StatusUpdateState,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ulid } from "~/services/ulid.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "PUT") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { runId, id } = ParamsSchema.parse(params);
|
||||
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
logger.debug("SetStatusService.call() request body", {
|
||||
body: anyBody,
|
||||
runId,
|
||||
id,
|
||||
});
|
||||
|
||||
const body = StatusUpdateSchema.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new SetStatusService();
|
||||
|
||||
try {
|
||||
const statusRecord = await service.call(runId, id, body.data);
|
||||
|
||||
logger.debug("SetStatusService.call() response body", {
|
||||
runId,
|
||||
id,
|
||||
statusRecord,
|
||||
});
|
||||
|
||||
if (!statusRecord) {
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
|
||||
const status = JobRunStatusRecordSchema.parse({
|
||||
...statusRecord,
|
||||
state: statusRecord.state ?? undefined,
|
||||
history: statusRecord.history ?? undefined,
|
||||
data: statusRecord.data ?? undefined,
|
||||
});
|
||||
|
||||
return json(status);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export class SetStatusService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(runId: string, id: string, status: StatusUpdate) {
|
||||
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
|
||||
const existingStatus = await tx.jobRunStatusRecord.findUnique({
|
||||
where: {
|
||||
runId_key: {
|
||||
runId,
|
||||
key: id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const history: StatusHistory = [];
|
||||
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
|
||||
if (historyResult.success) {
|
||||
history.push(...historyResult.data);
|
||||
}
|
||||
if (existingStatus) {
|
||||
history.push({
|
||||
label: existingStatus.label,
|
||||
state: (existingStatus.state ?? undefined) as StatusUpdateState,
|
||||
data: (existingStatus.data ?? undefined) as StatusUpdateData,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedStatus = await tx.jobRunStatusRecord.upsert({
|
||||
where: {
|
||||
runId_key: {
|
||||
runId,
|
||||
key: id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
key: id,
|
||||
runId,
|
||||
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
|
||||
label: status.label ?? id,
|
||||
state: status.state,
|
||||
data: status.data as any,
|
||||
history: [],
|
||||
},
|
||||
update: {
|
||||
label: status.label,
|
||||
state: status.state,
|
||||
data: status.data as any,
|
||||
history: history as any[],
|
||||
},
|
||||
});
|
||||
|
||||
return updatedStatus;
|
||||
});
|
||||
|
||||
return statusRecord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
const RecordsSchema = z.array(JobRunStatusRecordSchema);
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
if (request.method.toUpperCase() === "OPTIONS") {
|
||||
return apiCors(request, json({}));
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request, { allowPublicKey: true });
|
||||
|
||||
if (!authenticationResult) {
|
||||
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
|
||||
}
|
||||
|
||||
const { runId } = ParamsSchema.parse(params);
|
||||
|
||||
logger.debug("Get run statuses", {
|
||||
runId,
|
||||
});
|
||||
|
||||
try {
|
||||
const run = await prisma.jobRun.findUnique({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
output: true,
|
||||
statuses: {
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return apiCors(request, json({ error: `No run found for id ${runId}` }, { status: 404 }));
|
||||
}
|
||||
|
||||
const parsedStatuses = RecordsSchema.parse(
|
||||
run.statuses.map((s) => ({
|
||||
...s,
|
||||
state: s.state ?? undefined,
|
||||
data: s.data ?? undefined,
|
||||
history: s.history ?? undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
return apiCors(
|
||||
request,
|
||||
json({
|
||||
run: {
|
||||
id: run.id,
|
||||
status: run.status,
|
||||
output: run.output,
|
||||
},
|
||||
statuses: parsedStatuses,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return apiCors(request, json({ error: error.message }, { status: 400 }));
|
||||
}
|
||||
|
||||
return apiCors(request, json({ error: "Something went wrong" }, { status: 500 }));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
|
||||
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
|
||||
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// Using a transaction, we'll first check to see if the task already exists and return if if it does
|
||||
// If it doesn't exist, we'll create it and return it
|
||||
const task = await this.#prismaClient.$transaction(async (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,31 @@ export class CompleteRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class FailRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// Using a transaction, we'll first check to see if the task already exists and return if if it does
|
||||
// If it doesn't exist, we'll create it and return it
|
||||
const task = await this.#prismaClient.$transaction(async (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,31 @@ export class FailRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { cors } from "remix-utils";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest, getApiKeyFromRequest } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
import { taskListToTree } from "~/utils/taskListToTree";
|
||||
|
||||
@@ -93,6 +92,9 @@ export async function loader({ request, params }: LoaderArgs) {
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
statuses: {
|
||||
select: { key: true, label: true, state: true, data: true, history: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,6 +124,12 @@ export async function loader({ request, params }: LoaderArgs) {
|
||||
const { parentId, ...rest } = task;
|
||||
return { ...rest };
|
||||
}),
|
||||
statuses: jobRun.statuses.map((s) => ({
|
||||
...s,
|
||||
state: s.state ?? undefined,
|
||||
data: s.data ?? undefined,
|
||||
history: s.history ?? undefined,
|
||||
})),
|
||||
nextCursor: nextTask ? nextTask.id : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -7,10 +7,7 @@ import { logger } from "../logger.server";
|
||||
export class IngestSendEvent {
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
|
||||
constructor(
|
||||
prismaClient: PrismaClientOrTransaction = prisma,
|
||||
private deliverEvents = true
|
||||
) {
|
||||
constructor(prismaClient: PrismaClientOrTransaction = prisma, private deliverEvents = true) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
@@ -37,71 +34,55 @@ export class IngestSendEvent {
|
||||
try {
|
||||
const deliverAt = this.#calculateDeliverAt(options);
|
||||
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Create a new event in the database
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
connect: {
|
||||
id: environment.projectId,
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
eventId: event.id,
|
||||
name: event.name,
|
||||
timestamp: event.timestamp ?? new Date(),
|
||||
payload: event.payload ?? {},
|
||||
context: event.context ?? {},
|
||||
source: event.source ?? "trigger.dev",
|
||||
sourceContext,
|
||||
deliverAt: deliverAt,
|
||||
externalAccount: externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: externalAccount.id,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
// Create a new event in the database
|
||||
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 ?? {},
|
||||
context: event.context ?? {},
|
||||
source: event.source ?? "trigger.dev",
|
||||
sourceContext,
|
||||
deliverAt: deliverAt,
|
||||
externalAccountId: externalAccount ? externalAccount.id : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
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}` }
|
||||
);
|
||||
}
|
||||
|
||||
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}` }
|
||||
);
|
||||
}
|
||||
|
||||
return eventLog;
|
||||
},
|
||||
{ rethrowPrismaErrors: true }
|
||||
);
|
||||
return eventLog;
|
||||
});
|
||||
} catch (error) {
|
||||
const prismaError = PrismaErrorSchema.safeParse(error);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { resend } from "./integrations/resend";
|
||||
@@ -33,6 +34,7 @@ export class IntegrationCatalog {
|
||||
export const integrationCatalog = new IntegrationCatalog({
|
||||
airtable,
|
||||
github,
|
||||
linear,
|
||||
openai,
|
||||
plain,
|
||||
resend,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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)],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,14 @@ import {
|
||||
SCHEDULED_EVENT,
|
||||
TriggerMetadata,
|
||||
} from "@trigger.dev/core";
|
||||
import type { Endpoint, Integration, Job, JobIntegration, JobVersion } from "@trigger.dev/database";
|
||||
import type {
|
||||
Endpoint,
|
||||
Integration,
|
||||
Job,
|
||||
JobIntegration,
|
||||
JobIntegrationPayload,
|
||||
JobVersion,
|
||||
} from "@trigger.dev/database";
|
||||
import { DEFAULT_MAX_CONCURRENT_RUNS } from "~/consts";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
@@ -62,83 +69,7 @@ export class RegisterJobService {
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
if (jobIntegration.authSource === "LOCAL") {
|
||||
integration = await this.#prismaClient.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId: environment.organizationId,
|
||||
slug: jobIntegration.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
integration = await this.#prismaClient.integration.create({
|
||||
data: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.id,
|
||||
authSource: "HOSTED",
|
||||
setupStatus: "MISSING_FIELDS",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
integration = await this.#upsertIntegrationForJobIntegration(environment, jobIntegration);
|
||||
}
|
||||
|
||||
integrations.set(jobIntegration.id, integration);
|
||||
@@ -472,6 +403,7 @@ export class RegisterJobService {
|
||||
key: job.id,
|
||||
dispatcher: eventDispatcher,
|
||||
schedule: trigger.schedule,
|
||||
organizationId: job.organizationId,
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -479,6 +411,145 @@ export class RegisterJobService {
|
||||
}
|
||||
}
|
||||
|
||||
async #upsertIntegrationForJobIntegration(
|
||||
environment: AuthenticatedEnvironment,
|
||||
jobIntegration: IntegrationConfig
|
||||
): Promise<Integration> {
|
||||
switch (jobIntegration.authSource) {
|
||||
case "LOCAL": {
|
||||
return await this.#prismaClient.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId: environment.organizationId,
|
||||
slug: jobIntegration.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
case "HOSTED": {
|
||||
return await this.#prismaClient.integration.create({
|
||||
data: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.id,
|
||||
authSource: "HOSTED",
|
||||
setupStatus: "MISSING_FIELDS",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
case "RESOLVER": {
|
||||
return await this.#prismaClient.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId: environment.organizationId,
|
||||
slug: jobIntegration.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "RESOLVER",
|
||||
connectionType: "EXTERNAL",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "RESOLVER",
|
||||
connectionType: "EXTERNAL",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(jobIntegration.authSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #upsertJobIntegration(
|
||||
job: Job & {
|
||||
integrations: Array<JobIntegration & { integration: Integration | null }>;
|
||||
@@ -572,3 +643,7 @@ export class RegisterJobService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function assertExhaustive(x: never): never {
|
||||
throw new Error("Unexpected object: " + x);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ export class TestJobService {
|
||||
environmentId,
|
||||
versionId,
|
||||
payload,
|
||||
accountId,
|
||||
}: {
|
||||
environmentId: string;
|
||||
versionId: string;
|
||||
payload: any;
|
||||
payload?: any;
|
||||
accountId?: string;
|
||||
}) {
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
@@ -41,10 +43,27 @@ export class TestJobService {
|
||||
},
|
||||
});
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const event = EventSpecificationSchema.parse(version.eventSpecification);
|
||||
const eventName = Array.isArray(event.name) ? event.name[0] : event.name;
|
||||
|
||||
const eventLog = await this.#prismaClient.eventRecord.create({
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organization: {
|
||||
connect: {
|
||||
@@ -61,6 +80,13 @@ export class TestJobService {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
externalAccount: externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: externalAccount.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
eventId: `test:${eventName}:${new Date().getTime()}`,
|
||||
name: eventName,
|
||||
timestamp: new Date(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { $transaction, Prisma, PrismaClient, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
|
||||
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "ABORTED", "CANCELED"];
|
||||
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "UNRESOLVED_AUTH", "ABORTED", "CANCELED"];
|
||||
|
||||
export class ContinueRunService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -42,29 +42,32 @@ export class CreateRunService {
|
||||
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
// Get the current max number for the given jobId
|
||||
const currentMaxNumber = await tx.jobRun.aggregate({
|
||||
const latestJob = await tx.jobRun.findFirst({
|
||||
where: { jobId: job.id },
|
||||
_max: { number: true },
|
||||
orderBy: { id: "desc" },
|
||||
select: {
|
||||
number: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Increment the number for the new execution
|
||||
const newNumber = (currentMaxNumber._max.number ?? 0) + 1;
|
||||
const newNumber = (latestJob?.number ?? 0) + 1;
|
||||
|
||||
// Create the new execution with the incremented number
|
||||
const run = await tx.jobRun.create({
|
||||
data: {
|
||||
number: newNumber,
|
||||
preprocess: version.preprocessRuns,
|
||||
job: { connect: { id: job.id } },
|
||||
version: { connect: { id: version.id } },
|
||||
event: { connect: { id: eventId } },
|
||||
environment: { connect: { id: environment.id } },
|
||||
organization: { connect: { id: environment.organizationId } },
|
||||
project: { connect: { id: environment.projectId } },
|
||||
endpoint: { connect: { id: endpoint.id } },
|
||||
queue: { connect: { id: jobQueue.id } },
|
||||
externalAccount: eventRecord.externalAccountId
|
||||
? { connect: { id: eventRecord.externalAccountId } }
|
||||
jobId: job.id,
|
||||
versionId: version.id,
|
||||
eventId: eventId,
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
projectId: environment.projectId,
|
||||
endpointId: endpoint.id,
|
||||
queueId: jobQueue.id,
|
||||
externalAccountId: eventRecord.externalAccountId
|
||||
? eventRecord.externalAccountId
|
||||
: undefined,
|
||||
isTest: eventRecord.isTest,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
CachedTaskSchema,
|
||||
RunJobError,
|
||||
RunJobInvalidPayloadError,
|
||||
RunJobResumeWithTask,
|
||||
RunJobRetryWithTask,
|
||||
RunJobSuccess,
|
||||
RunJobUnresolvedAuthError,
|
||||
RunSourceContextSchema,
|
||||
} from "@trigger.dev/core";
|
||||
import type { Task } from "@trigger.dev/database";
|
||||
@@ -342,6 +344,16 @@ export class PerformRunExecutionV1Service {
|
||||
await this.#cancelExecution(execution);
|
||||
break;
|
||||
}
|
||||
case "UNRESOLVED_AUTH_ERROR": {
|
||||
await this.#failRunWithUnresolvedAuthError(execution, safeBody.data);
|
||||
|
||||
break;
|
||||
}
|
||||
case "INVALID_PAYLOAD": {
|
||||
await this.#failRunWithInvalidPayloadError(execution, safeBody.data);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
@@ -438,6 +450,24 @@ export class PerformRunExecutionV1Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithUnresolvedAuthError(
|
||||
execution: FoundRunExecution,
|
||||
data: RunJobUnresolvedAuthError
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(tx, execution, data.issues, "UNRESOLVED_AUTH");
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithInvalidPayloadError(
|
||||
execution: FoundRunExecution,
|
||||
data: RunJobInvalidPayloadError
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(tx, execution, data.errors, "INVALID_PAYLOAD");
|
||||
});
|
||||
}
|
||||
|
||||
async #retryRunWithTask(execution: FoundRunExecution, data: RunJobRetryWithTask) {
|
||||
const { run } = execution;
|
||||
|
||||
@@ -557,7 +587,7 @@ export class PerformRunExecutionV1Service {
|
||||
prisma: PrismaClientOrTransaction,
|
||||
execution: FoundRunExecution,
|
||||
output: Record<string, any>,
|
||||
status: "FAILURE" | "ABORTED" = "FAILURE"
|
||||
status: "FAILURE" | "ABORTED" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE"
|
||||
): Promise<void> {
|
||||
const { run } = execution;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
CachedTask,
|
||||
RunJobError,
|
||||
RunJobInvalidPayloadError,
|
||||
RunJobResumeWithTask,
|
||||
RunJobRetryWithTask,
|
||||
RunJobSuccess,
|
||||
RunJobUnresolvedAuthError,
|
||||
RunSourceContextSchema,
|
||||
} from "@trigger.dev/core";
|
||||
import { RuntimeEnvironmentType, type Task } from "@trigger.dev/database";
|
||||
@@ -151,6 +153,29 @@ export class PerformRunExecutionV2Service {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
typeof process.env.BLOCKED_ORGS === "string" &&
|
||||
process.env.BLOCKED_ORGS.includes(run.organizationId)
|
||||
) {
|
||||
logger.debug("Skipping execution for blocked org", {
|
||||
orgId: run.organizationId,
|
||||
});
|
||||
|
||||
await this.#prismaClient.jobRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
status: "CANCELED",
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
|
||||
const event = eventRecordToApiJson(run.event);
|
||||
|
||||
@@ -354,6 +379,16 @@ export class PerformRunExecutionV2Service {
|
||||
await this.#cancelExecution(run);
|
||||
break;
|
||||
}
|
||||
case "UNRESOLVED_AUTH_ERROR": {
|
||||
await this.#failRunWithUnresolvedAuthError(run, safeBody.data, durationInMs);
|
||||
|
||||
break;
|
||||
}
|
||||
case "INVALID_PAYLOAD": {
|
||||
await this.#failRunWithInvalidPayloadError(run, safeBody.data, durationInMs);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
@@ -432,6 +467,40 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithUnresolvedAuthError(
|
||||
execution: FoundRun,
|
||||
data: RunJobUnresolvedAuthError,
|
||||
durationInMs: number
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
execution,
|
||||
data.issues,
|
||||
"UNRESOLVED_AUTH",
|
||||
durationInMs
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithInvalidPayloadError(
|
||||
execution: FoundRun,
|
||||
data: RunJobInvalidPayloadError,
|
||||
durationInMs: number
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
execution,
|
||||
data.errors,
|
||||
"INVALID_PAYLOAD",
|
||||
durationInMs
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async #retryRunWithTask(
|
||||
run: FoundRun,
|
||||
data: RunJobRetryWithTask,
|
||||
@@ -556,7 +625,7 @@ export class PerformRunExecutionV2Service {
|
||||
reason: "EXECUTE_JOB" | "PREPROCESS",
|
||||
run: FoundRun,
|
||||
output: Record<string, any>,
|
||||
status: "FAILURE" | "ABORTED" | "TIMED_OUT" = "FAILURE",
|
||||
status: "FAILURE" | "ABORTED" | "TIMED_OUT" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE",
|
||||
durationInMs: number = 0
|
||||
): Promise<void> {
|
||||
await $transaction(prisma, async (tx) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ export class ReRunService {
|
||||
version: true,
|
||||
job: true,
|
||||
event: true,
|
||||
externalAccount: true,
|
||||
},
|
||||
where: {
|
||||
id: runId,
|
||||
@@ -43,6 +44,13 @@ export class ReRunService {
|
||||
id: existingRun.environment.id,
|
||||
},
|
||||
},
|
||||
externalAccount: existingRun.externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: existingRun.externalAccount.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
eventId: `${existingRun.event.eventId}:retry:${new Date().getTime()}`,
|
||||
name: existingRun.event.name,
|
||||
timestamp: new Date(),
|
||||
|
||||
@@ -50,11 +50,11 @@ export class StartRunService {
|
||||
integrationId: runConnection.integration.id,
|
||||
authSource: "HOSTED",
|
||||
} as const)
|
||||
: runConnection.result === "resolvedLocal"
|
||||
: runConnection.result === "resolvedLocal" || runConnection.result === "resolvedResolver"
|
||||
? ({
|
||||
key,
|
||||
integrationId: runConnection.integration.id,
|
||||
authSource: "LOCAL",
|
||||
authSource: runConnection.result === "resolvedLocal" ? "LOCAL" : "RESOLVER",
|
||||
} as const)
|
||||
: undefined
|
||||
)
|
||||
@@ -173,6 +173,7 @@ async function createRunConnections(tx: PrismaClientOrTransaction, run: FoundRun
|
||||
integration: Integration;
|
||||
}
|
||||
| { result: "resolvedLocal"; integration: Integration }
|
||||
| { result: "resolvedResolver"; integration: Integration }
|
||||
| {
|
||||
result: "missing";
|
||||
connectionType: ConnectionType;
|
||||
@@ -190,6 +191,11 @@ async function createRunConnections(tx: PrismaClientOrTransaction, run: FoundRun
|
||||
result: "resolvedLocal",
|
||||
integration: jobIntegration.integration,
|
||||
};
|
||||
} else if (jobIntegration.integration.authSource === "RESOLVER") {
|
||||
acc[jobIntegration.key] = {
|
||||
result: "resolvedResolver",
|
||||
integration: jobIntegration.integration,
|
||||
};
|
||||
} else {
|
||||
const connection = run.externalAccountId
|
||||
? await tx.integrationConnection.findFirst({
|
||||
|
||||
@@ -59,6 +59,7 @@ export class RegisterScheduleService {
|
||||
schedule: payload,
|
||||
accountId: payload.accountId,
|
||||
dynamicTrigger,
|
||||
organizationId: environment.organizationId,
|
||||
});
|
||||
|
||||
return registration;
|
||||
|
||||
@@ -16,24 +16,32 @@ export class RegisterScheduleSourceService {
|
||||
schedule,
|
||||
accountId,
|
||||
dynamicTrigger,
|
||||
organizationId,
|
||||
}: {
|
||||
key: string;
|
||||
dispatcher: EventDispatcher;
|
||||
schedule: ScheduleMetadata;
|
||||
accountId?: string;
|
||||
dynamicTrigger?: DynamicTrigger;
|
||||
organizationId: string;
|
||||
}) {
|
||||
const validatedSchedule = validateSchedule(schedule);
|
||||
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: dispatcher.environmentId,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: dispatcher.environmentId,
|
||||
organizationId: organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -71,13 +71,19 @@ export class RegisterSourceServiceV1 {
|
||||
}
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -71,13 +71,19 @@ export class RegisterSourceServiceV2 {
|
||||
}
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ export class RegisterTriggerSourceServiceV2 {
|
||||
endpointSlug,
|
||||
id,
|
||||
key,
|
||||
accountId,
|
||||
registrationMetadata,
|
||||
}: {
|
||||
environment: AuthenticatedEnvironment;
|
||||
@@ -32,7 +31,6 @@ export class RegisterTriggerSourceServiceV2 {
|
||||
id: string;
|
||||
endpointSlug: string;
|
||||
key: string;
|
||||
accountId?: string;
|
||||
registrationMetadata?: any;
|
||||
}): Promise<RegisterSourceEventV2 | undefined> {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
@@ -63,7 +61,7 @@ export class RegisterTriggerSourceServiceV2 {
|
||||
endpoint.id,
|
||||
payload.source,
|
||||
dynamicTrigger.id,
|
||||
accountId,
|
||||
payload.accountId,
|
||||
{ id: key, metadata: registrationMetadata }
|
||||
);
|
||||
|
||||
|
||||
@@ -161,7 +161,8 @@ function getWorkerQueue() {
|
||||
tasks: {
|
||||
"events.invokeDispatcher": {
|
||||
priority: 0, // smaller number = higher priority
|
||||
maxAttempts: 3,
|
||||
maxAttempts: 6,
|
||||
queueName: (payload) => `dispatcher:${payload.id}`, // use a queue for a dispatcher so runs are created sequentially
|
||||
handler: async (payload, job) => {
|
||||
const service = new InvokeDispatcherService();
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Redacts the given object based on the given paths
|
||||
// Example:
|
||||
// const redactor = new Redactor(["data.object.balance_transaction"]);
|
||||
// redactor.redact({
|
||||
// data: {
|
||||
// object: {
|
||||
// balance_transaction: "txn_1NYWgTI0XSgju2urW3aXpinM",
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// Returns:
|
||||
// {
|
||||
// data: {
|
||||
// object: {
|
||||
// balance_transaction: "[REDACTED]",
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// Does not currenly support arrays
|
||||
export class Redactor {
|
||||
constructor(private paths: string[]) {}
|
||||
|
||||
public redact(subject: unknown): unknown {
|
||||
if (!Array.isArray(this.paths)) {
|
||||
return subject;
|
||||
}
|
||||
|
||||
if (this.paths.length === 0) {
|
||||
return subject;
|
||||
}
|
||||
|
||||
const clonedSubject = JSON.parse(JSON.stringify(subject));
|
||||
|
||||
return this.redactPathsRecursive(clonedSubject, this.paths);
|
||||
}
|
||||
|
||||
private redactPathsRecursive(subject: any, paths: string[]): any {
|
||||
for (let path of paths) {
|
||||
let parts = path.split(".");
|
||||
|
||||
let curSubject = subject;
|
||||
|
||||
// Make sure curSubject is an object
|
||||
if (typeof curSubject !== "object") {
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(curSubject, part) === false) {
|
||||
// Path is not found in object
|
||||
break;
|
||||
}
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
// We're at the end of our path and have a string, redact it
|
||||
curSubject[part] = "[REDACTED]";
|
||||
} else if (part in curSubject && typeof curSubject[part] === "object") {
|
||||
// More paths to follow, continue down the path
|
||||
curSubject = curSubject[part];
|
||||
} else {
|
||||
// Path is not found in object or doesn't point to a string
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,8 @@
|
||||
"@remix-run/server-runtime": "1.19.2-pre.0",
|
||||
"@team-plain/typescript-sdk": "^2.2.0",
|
||||
"@trigger.dev/companyicons": "^1.5.14",
|
||||
"@trigger.dev/database": "workspace:*",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/database": "workspace:*",
|
||||
"@trigger.dev/sdk": "workspace:*",
|
||||
"@uiw/react-codemirror": "^4.19.5",
|
||||
"class-variance-authority": "^0.5.2",
|
||||
@@ -73,7 +73,6 @@
|
||||
"cuid": "^2.1.8",
|
||||
"emails": "workspace:*",
|
||||
"express": "^4.18.1",
|
||||
"fast-redact": "^3.1.2",
|
||||
"framer-motion": "^10.12.11",
|
||||
"graphile-worker": "^0.13.0",
|
||||
"highlight.run": "^7.3.4",
|
||||
@@ -96,6 +95,7 @@
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-use": "^17.4.0",
|
||||
"recharts": "^2.8.0",
|
||||
"remix-auth": "^3.2.2",
|
||||
"remix-auth-email-link": "^1.4.2",
|
||||
"remix-auth-github": "^1.1.1",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { integrationCatalog } from "../app/services/externalApis/integrationCatalog.server";
|
||||
import { seedCloud } from "./seedCloud";
|
||||
import { prisma } from "../app/db.server";
|
||||
import { createEnvironment } from "~/models/organization.server";
|
||||
|
||||
async function seedIntegrationAuthMethods() {
|
||||
for (const [_, integration] of Object.entries(integrationCatalog.getIntegrations())) {
|
||||
@@ -67,12 +68,78 @@ async function seedIntegrationAuthMethods() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runDataMigrations() {
|
||||
await runStagingEnvironmentMigration();
|
||||
}
|
||||
|
||||
async function runStagingEnvironmentMigration() {
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const existingDataMigration = await tx.dataMigration.findUnique({
|
||||
where: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
});
|
||||
|
||||
if (existingDataMigration) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tx.dataMigration.create({
|
||||
data: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Running data migration 2023-09-27-AddStagingEnvironments");
|
||||
|
||||
const projectsWithoutStagingEnvironments = await tx.project.findMany({
|
||||
where: {
|
||||
environments: {
|
||||
none: {
|
||||
type: "STAGING",
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const project of projectsWithoutStagingEnvironments) {
|
||||
try {
|
||||
console.log(
|
||||
`Creating staging environment for project ${project.slug} on org ${project.organization.slug}`
|
||||
);
|
||||
|
||||
await createEnvironment(project.organization, project, "STAGING", undefined, tx);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
await tx.dataMigration.update({
|
||||
where: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
data: {
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
await seedIntegrationAuthMethods();
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.SEED_CLOUD === "enabled") {
|
||||
await seedCloud(prisma);
|
||||
}
|
||||
|
||||
await runDataMigrations();
|
||||
}
|
||||
|
||||
seed()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<Card title="React hooks" icon="fishing-rod" href="/documentation/guides/react-hooks">
|
||||
Show the live status of Job Runs in your React app
|
||||
</Card>
|
||||
@@ -0,0 +1,5 @@
|
||||
<Accordion title="How do I get a Run id?">
|
||||
You can call [client.getRuns()](/sdk/triggerclient/instancemethods/getruns) with a Job id to get a
|
||||
list of the most recent Runs for that Job. You can then pass that run id to your frontend to use
|
||||
in the hook.
|
||||
</Accordion>
|
||||
@@ -0,0 +1,15 @@
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/slack@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/slack@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/slack@latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,49 @@
|
||||
<ParamField body="options" type="object" required>
|
||||
<Expandable title="properties" defaultOpen>
|
||||
<ParamField body="id" type="string" required>
|
||||
The `id` property is used to uniquely identify the Job. Only change this if you want to create a new Job.
|
||||
</ParamField>
|
||||
<ParamField body="name" type="string" required>
|
||||
The `name` of the Job that you want to appear in the dashboard and logs. You can change this without creating a new Job.
|
||||
</ParamField>
|
||||
<ParamField body="version" type="string" required>
|
||||
The `version` property is used to version your Job. A new version will be created if you change this property. We recommend using [semantic versioning](https://www.baeldung.com/cs/semantic-versioning), e.g. `1.0.3`.
|
||||
</ParamField>
|
||||
<ParamField body="trigger" type="object" required>
|
||||
The `trigger` property is used to define when the Job should run. There are currently the following Trigger types:
|
||||
- [cronTrigger](/sdk/crontrigger)
|
||||
- [intervalTrigger](/sdk/intervaltrigger)
|
||||
- [eventTrigger](/sdk/eventtrigger)
|
||||
- [DynamicTrigger](/sdk/dynamictrigger)
|
||||
- [DynamicSchedule](/sdk/dynamicschedule)
|
||||
- integration Triggers, like webhooks. See the [integrations](/integrations) page for more information.
|
||||
</ParamField>
|
||||
<ParamField body="run" type="function" required>
|
||||
This function gets called automatically when a Run is Triggered. It has three parameters:
|
||||
1. `payload` – The payload that was sent to the Trigger API.
|
||||
2. [io](/sdk/io) – An object that contains the integrations that you specified in the `integrations` property and other useful functions like delays and running Tasks.
|
||||
3. [context](/sdk/context) – An object that contains information about the Organization, Job, Run and more.
|
||||
|
||||
This is where you put the code you want to run for a Job. You can use normal code in here and you can also use Tasks.
|
||||
|
||||
You can return a value from this function and it will be sent back to the Trigger API.
|
||||
</ParamField>
|
||||
<ParamField body="integrations" type="object">
|
||||
Imports the specified integrations into the Job. The integrations will be available on the `io` object in the `run()` function with the same name as the key. For example:
|
||||
<Snippet file="how-to-pass-integrations.mdx" />
|
||||
</ParamField>
|
||||
<ParamField body="enabled" type="boolean">
|
||||
The `enabled` property is an optional property that specifies whether the Job is enabled or not. The Job will be enabled by default if you omit this property. When a job is disabled, no new runs will be triggered or resumed. In progress runs will continue to run until they are finished or delayed by using `io.wait`.
|
||||
</ParamField>
|
||||
<ParamField body="logLevel" type="log | error | warn | info | debug">
|
||||
The `logLevel` property is an optional property that specifies the level of
|
||||
logging for the Job. The level is inherited from the client if you omit this property.
|
||||
- `log` - logs only essential messages
|
||||
- `error` - logs error messages
|
||||
- `warn` - logs errors and warning messages
|
||||
- `info` - logs errors, warnings and info messages
|
||||
- `debug` - logs everything with full verbosity
|
||||
</ParamField>
|
||||
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
@@ -0,0 +1,4 @@
|
||||
<Info>
|
||||
Trigger.dev lets you create long-running jobs in serverless environments. Support for long-running
|
||||
servers is [coming soon](https://github.com/triggerdotdev/trigger.dev/discussions/430).
|
||||
</Info>
|
||||
@@ -1 +1,220 @@
|
||||
We're in the process of building support for the Astro framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues).
|
||||
## Install Required Packages
|
||||
|
||||
To begin, install the necessary packages in your Astro project directory. You can choose one of the following package managers:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i @trigger.dev/sdk@latest @trigger.dev/astro@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk@latest @trigger.dev/astro@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk@latest @trigger.dev/astro@latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Obtaining the Development API Key
|
||||
|
||||
To locate your development API key, login to the [Trigger.dev
|
||||
dashboard](https://cloud.trigger.dev) and select the Project you want to
|
||||
connect to. Then click on the Environments & API Keys tab in the left menu.
|
||||
You can copy your development API Key from the field at the top of this page.
|
||||
(Your development key will start with `tr_dev_`).
|
||||
|
||||
## Adding Environment Variables
|
||||
|
||||
Create a `.env` file at the root of your project and include your Trigger API key and URL like this:
|
||||
|
||||
```bash
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
```
|
||||
|
||||
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
Create a file at `<root>/trigger.ts` or `<root>/src/trigger.ts`, depending on if your project uses a `src` directory, where `<root>` represents the root directory of your project.
|
||||
|
||||
Next, add the following code to the file which creates and exports a new `TriggerClient`:
|
||||
|
||||
```typescript src/trigger.ts
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-astro-app",
|
||||
apiKey: import.meta.env.TRIGGER_API_KEY,
|
||||
apiUrl: import.meta.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-astro-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Update the astro.config file to enable SSR (Server Side Rendering)
|
||||
|
||||
- You need to enable SSR to use API endpoints (which are required by Trigger.dev).
|
||||
|
||||
```typescript astro.config.mjs
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
//alternatively you can use "hybrid" instead of "server"
|
||||
output: "server",
|
||||
});
|
||||
```
|
||||
|
||||
To learn more about SSR, head over to the [Astro docs on SSR](https://docs.astro.build/en/guides/server-side-rendering/).
|
||||
|
||||
## Creating an Example Job
|
||||
|
||||
1. Create a folder named `jobs` alongside your `pages` directory
|
||||
2. Inside the `jobs` folder, add two files named `example.ts` and `index.ts`.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript src/jobs/example.ts
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "../trigger";
|
||||
|
||||
// your first job
|
||||
client.defineJob({
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("Hello world!", { payload });
|
||||
|
||||
return {
|
||||
message: "Hello world!",
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```typescript src/jobs/index.ts
|
||||
// export all your job files here
|
||||
export * from "./example";
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Creating the API Route
|
||||
|
||||
To establish an API route for interacting with Trigger.dev, follow these steps based on your project's file type and structure
|
||||
|
||||
1. Create a new file named `trigger.ts` within the `pages/api/` directory.
|
||||
2. Add the following code to `trigger.ts`:
|
||||
|
||||
```typescript src/pages/api/trigger.ts
|
||||
import { createAstroRoute } from "@trigger.dev/astro";
|
||||
//you may need to update this path to point at your trigger.ts file
|
||||
import { client } from "../../trigger";
|
||||
|
||||
//import your jobs, this could be different depending on your project structure
|
||||
import "../../jobs";
|
||||
|
||||
export const prerender = false;
|
||||
export const { POST } = createAstroRoute(client);
|
||||
```
|
||||
|
||||
## Adding Configuration to `package.json`
|
||||
|
||||
Inside the `package.json` file, add the following configuration under the root object:
|
||||
|
||||
```json
|
||||
"trigger.dev": {
|
||||
"endpointId": "my-astro-app"
|
||||
}
|
||||
```
|
||||
|
||||
Your `package.json` file might look something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
// ... other dependencies
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "my-astro-app"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace **"my-astro-app"** with the appropriate identifier you used during the step for creating the `TriggerClient`.
|
||||
|
||||
## Additonal Job Definitions
|
||||
|
||||
You can define more job definitions by creating additional files in the `jobs` folder and exporting them in `index` file.
|
||||
|
||||
For example, in `index.ts`, you can export other job files like this:
|
||||
|
||||
```typescript
|
||||
// import all your job files here
|
||||
|
||||
export * from "./examples";
|
||||
export * from "./other-job-file";
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
### Run your Astro app
|
||||
|
||||
Run your Astro app locally, like you normally would. For example:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Run the CLI 'dev' command
|
||||
|
||||
In a **_separate terminal window or tab_** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev --port 4321
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev --port 4321
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev --port 4321
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
Astro by default runs on port 4321.
|
||||
</Note>
|
||||
<Note>
|
||||
You can optionally pass the hostname if you're not running on localhost by adding
|
||||
`--hostname <host>`. Example, in case your Astro app is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
</Note>
|
||||
|
||||
### Next Steps
|
||||
|
||||
You should now see your example job in the Trigger.dev dashboard. You can now create additional jobs and use the Trigger.dev dashboard to test them.
|
||||
|
||||
@@ -1 +1,193 @@
|
||||
We're in the process of building support for the Express framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues).
|
||||
## Installing Required Packages
|
||||
|
||||
Start by installing the necessary packages in your Express.js project directory. You can use npm, pnpm, or yarn as your package manager.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/sdk @trigger.dev/express
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/express
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger.dev/express
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<Note>Ensure that you execute this command within a Express project.</Note>
|
||||
|
||||
## Obtaining the Development Server API Key
|
||||
|
||||
To locate your development Server API key, login to the [Trigger.dev
|
||||
dashboard](https://cloud.trigger.dev) and select the Project you want to
|
||||
connect to. Then click on the Environments & API Keys tab in the left menu.
|
||||
You can copy your development Server API Key from the field at the top of this page.
|
||||
(Your development key will start with `tr_dev_`).
|
||||
|
||||
## Adding Environment Variables
|
||||
|
||||
Create a `.env` file at the root of your project and include your Trigger API key and URL like this:
|
||||
|
||||
```bash
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
```
|
||||
|
||||
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
Create a file for your Trigger client, in this case we create it at `<root>/trigger.(ts/js)`
|
||||
|
||||
```ts trigger.(ts/js)
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY!,
|
||||
apiUrl: process.env.TRIGGER_API_URL!,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Adding the API endpoint
|
||||
|
||||
There are a few different options depending on how your Express project is configured.
|
||||
|
||||
- App middleware
|
||||
- Entire app for Trigger.dev (only relevant if it's the only thing your project is for)
|
||||
|
||||
Select the appropriate code example from below:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript app middleware
|
||||
//import the client from the other file
|
||||
import { client } from "./trigger";
|
||||
import { createMiddleware } from "@trigger.dev/express";
|
||||
|
||||
//import your job files
|
||||
import "./jobs/example";
|
||||
|
||||
//..your existing Express code
|
||||
const app: Express = express();
|
||||
|
||||
//add the middleware
|
||||
app.use(createMiddleware(client));
|
||||
|
||||
//..the rest of your Express code
|
||||
```
|
||||
|
||||
```typescript entire app
|
||||
//if the entire app is just for Trigger.dev
|
||||
import { client } from "./trigger";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
|
||||
//import your job files
|
||||
import "./jobs/example";
|
||||
|
||||
//this creates an app
|
||||
createExpressServer(client);
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Creating the Example Job
|
||||
|
||||
Create a Job file. In this case created `<root>/jobs/example.(ts/js)`
|
||||
|
||||
```typescript jobs/example.(ts/js)
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "../trigger";
|
||||
|
||||
// your first job
|
||||
client.defineJob({
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("Hello world!", { payload });
|
||||
|
||||
return {
|
||||
message: "Hello world!",
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Adding Configuration to `package.json`
|
||||
|
||||
Inside the `package.json` file, add the following configuration under the root object:
|
||||
|
||||
```json
|
||||
"trigger.dev": {
|
||||
"endpointId": "my-app"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Replace **"my-app"** with the appropriate identifier you used in the trigger.js configuration file.
|
||||
|
||||
## Running
|
||||
|
||||
### Run your Express app
|
||||
|
||||
Run your Express app locally, like you normally would. For example:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Note>You might use `npm run start` instead of dev</Note>
|
||||
|
||||
### Run the CLI 'dev' command
|
||||
|
||||
In a **_separate terminal window or tab_** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
You can optionally pass the port if you're not running on 3000 by adding
|
||||
`--port 3001` to the end
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
You can optionally pass the hostname if you're not running on localhost by adding
|
||||
`--hostname <host>`. Example, in case your Express is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
</Note>
|
||||
|
||||
@@ -5,15 +5,15 @@ To begin, install the necessary packages in your Remix project directory. You ca
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i @trigger.dev/sdk @trigger-dev/remix
|
||||
npm i @trigger.dev/sdk @trigger.dev/remix
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger-dev/remix
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/remix
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger-dev/remix
|
||||
yarn add @trigger.dev/sdk @trigger.dev/remix
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -32,50 +32,30 @@ Create a `.env` file at the root of your project and include your Trigger API ke
|
||||
|
||||
```bash
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://cloud.trigger.dev
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
```
|
||||
|
||||
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
To set up the Trigger Client for your project, follow these steps:
|
||||
Create a file at `<root>/app/trigger.ts`, where `<root>` represents the root directory of your project.
|
||||
|
||||
1. **Create Configuration File:**
|
||||
Next, add the following code to the file which creates and exports a new `TriggerClient`:
|
||||
|
||||
In your project directory, create a configuration file named `trigger.ts` or `trigger.js`, depending on whether your project uses TypeScript (`.ts`) or JavaScript (`.js`).
|
||||
```typescript app/trigger.(ts/js)
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
2. **Choose Directory:**
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
Create the configuration file inside the **app** directory of your project.
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add Configuration Code:**
|
||||
|
||||
Open the configuration file you created and add the following code:
|
||||
|
||||
```typescript app/trigger.(ts/js)
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
|
||||
|
||||
4. **Example Directory Structure :**
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── app/
|
||||
├── routes/
|
||||
├── trigger.ts
|
||||
├── other files...
|
||||
```
|
||||
Replace **"my-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Creating the API Route
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
The CLI `dev` command allows the Trigger.dev service to send messages to your site. This is required for registering Jobs, triggering them and running tasks. To achieve this it creates a tunnel (using [ngrok](https://ngrok.com/)) so Trigger.dev can send messages to your machine.
|
||||
|
||||
You should leave the `dev` command running when you're developing.
|
||||
|
||||
In a **new terminal window or tab** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
You can optionally pass the port if you're not running on the default port by adding
|
||||
`--port 3001` to the end.
|
||||
</Note>
|
||||
@@ -0,0 +1,26 @@
|
||||
```typescript
|
||||
// Your first job
|
||||
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
|
||||
client.defineJob({
|
||||
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
|
||||
id: "example-job",
|
||||
name: "Example Job: a joke with a delay",
|
||||
version: "0.0.1",
|
||||
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This logs a message to the console
|
||||
await io.logger.info("🧪 Example Job: a joke with a delay");
|
||||
await io.logger.info("How do you comfort a JavaScript bug?");
|
||||
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
|
||||
await io.wait("Wait 5 seconds for the punchline...", 5);
|
||||
await io.logger.info("You console it! 🤦");
|
||||
await io.logger.info(
|
||||
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
|
||||
);
|
||||
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
<Step title="Triggering the Job">
|
||||
|
||||
There are two way to trigger this Job.
|
||||
|
||||
1. Use the "Test" functionality in the dashboard.
|
||||
2. Use the Trigger.dev API (either via our SDK or a web request)
|
||||
|
||||
#### "Testing" from the dashboard
|
||||
|
||||
Click into the Job and then open the "Test" tab. You should see this page:
|
||||
|
||||

|
||||
|
||||
This Job doesn't have a payload schema (meaning it takes an empty object), so you can simple click the "Run test" button.
|
||||
|
||||
**Congratulations, you should get redirected so you can see your first Run!**
|
||||
|
||||
</Step>
|
||||
@@ -0,0 +1,80 @@
|
||||
<Step title="Create a Trigger.dev account">
|
||||
|
||||
You can either:
|
||||
|
||||
- Use the [Trigger.dev Cloud](https://cloud.trigger.dev).
|
||||
- Or [self-host](/documentation/guides/self-hosting) the service.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create your first project">
|
||||
|
||||
Once you've created an account, follow the steps in the app to:
|
||||
|
||||
1. Complete your account details.
|
||||
2. Create your first Organization and Project.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Getting an API key">
|
||||
|
||||
1. Go to the "Environments & API Keys" page in your project.
|
||||

|
||||
|
||||
2. Copy the `DEV` **SERVER** API key.
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run the CLI `init` command">
|
||||
|
||||
The easiest way to get started it to use the CLI. It will add Trigger.dev to your existing project, setup a route and give you an example file.
|
||||
|
||||
In a terminal window run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
It will ask you a couple of questions
|
||||
|
||||
1. Are you using the [Trigger.dev Cloud](https://cloud.trigger.dev) or [self-hosting](/documentation/guides/self-hosting)?
|
||||
2. Enter your development API key. Enter the key you copied earlier.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run your site">
|
||||
|
||||
Make sure your site is running locally, we will connect to it to register your Jobs.
|
||||
|
||||
<Warning>You must leave this running for the rest of the steps.</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Step>
|
||||
@@ -0,0 +1,20 @@
|
||||
## What's next?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Write your first Job" icon="hexagon-plus" href="/documentation/guides/create-a-job">
|
||||
A Guide for how to create your first real Job
|
||||
</Card>
|
||||
<Card
|
||||
title="What is Trigger.dev"
|
||||
icon="wand-magic-sparkles"
|
||||
href="/documentation/concepts/what-is-triggerdotdev"
|
||||
>
|
||||
Learn more about how Trigger.dev works and how it can help you.
|
||||
</Card>
|
||||
<Card title="Examples" icon="slot-machine" href="/examples">
|
||||
One of the quickest ways to learn how Trigger.dev works is to view some example Jobs.
|
||||
</Card>
|
||||
<Card title="Get help" icon="hire-a-helper" href="/documentation/get-help">
|
||||
Struggling getting setup or have a question? We're here to help.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,24 @@
|
||||
## The two types of Run progress you can use
|
||||
|
||||
1. Automatic updates of Run and Task progress (no extra Job code required)
|
||||
2. Explicitly created and updated `statuses` (more flexible and powerful)
|
||||
|
||||
### Automatic updates
|
||||
|
||||
These require no changes inside your Job code. You can receive:
|
||||
|
||||
- Info about an event you sent, including the Runs it triggered.
|
||||
- The overall status of the Run (in progress, success and fail statuses).
|
||||
- Metadata like start and completed times.
|
||||
- The Run output (what is returned or an error that failed the Job)
|
||||
- Information about the Tasks that have completed/failed/are running.
|
||||
|
||||
### Explicit `statuses`
|
||||
|
||||
You can create `statuses` in your Job code. This gives you fine grained control over what you want to expose.
|
||||
|
||||
It allows you to:
|
||||
|
||||
- Show exactly what you want in your UI (with as many statuses as you want).
|
||||
- Pass arbitrary data to your UI, which you can use to render elements.
|
||||
- Update existing elements in your UI as the progress of the run continues.
|
||||
@@ -28,5 +28,6 @@ Each platform has one or more adaptors, see the guides below:
|
||||
| ------------------------------------------------- | -------------------- |
|
||||
| [Next.js](/documentation/guides/platforms/nextjs) | `createPagesRoute()` |
|
||||
| [Next.js](/documentation/guides/platforms/nextjs) | `createAppRoute()` |
|
||||
| [Astro](/documentation/guides/platforms/astro) | `createAstroRoute()` |
|
||||
| [Remix](/documentation/guides/platforms/remix) | `createRemixRoute()` |
|
||||
| Express | Coming soon |
|
||||
|
||||
@@ -27,6 +27,10 @@ The `DEV` environment should only be used for local development. It's where you
|
||||
|
||||
<Snippet file="scheduled-dev-warning.mdx" />
|
||||
|
||||
### Staging
|
||||
|
||||
The `STAGING` environment is useful for testing your Jobs against your staging server, if you have one. STAGING works identically to PROD.
|
||||
|
||||
### Production
|
||||
|
||||
The `PROD` environment is where your Jobs will run in production. It's where you can run your Jobs against real data.
|
||||
|
||||
@@ -4,7 +4,7 @@ title: "Limitations"
|
||||
|
||||
There are a few limitations that are important to understand.
|
||||
|
||||
In the current beta:
|
||||
In the latest version:
|
||||
|
||||
- Runs on localhost are limited to 5 minutes.
|
||||
- On long-running servers (not serverless) Runs can be retried erroneously.
|
||||
|
||||
@@ -12,7 +12,7 @@ Sometimes you don't know when you write the code what the trigger or schedule wi
|
||||
|
||||
```typescript
|
||||
//1. create a DynamicSchedule
|
||||
const dynamicSchedule = new DynamicSchedule(client, {
|
||||
const dynamicSchedule = client.defineDynamicSchedule({
|
||||
id: "dynamicinterval",
|
||||
});
|
||||
|
||||
@@ -53,15 +53,18 @@ client.defineJob({
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//6. Register the DynamicSchedule
|
||||
await io.registerInterval("📆", dynamicSchedule, payload.userId, {
|
||||
seconds: payload.seconds,
|
||||
//6. Register the DynamicSchedule (this will automatically create a task)
|
||||
await dynamicSchedule.register(userId, {
|
||||
type: "cron",
|
||||
options: {
|
||||
cron: userSchedule,
|
||||
},
|
||||
});
|
||||
|
||||
await io.wait("wait", 60);
|
||||
|
||||
//7. Unregister the DynamicSchedule if you want
|
||||
await io.unregisterInterval("❌📆", dynamicSchedule, payload.id);
|
||||
//7. Unregister the DynamicSchedule if you want (this will automatically create a task)
|
||||
await dynamicSchedule.unregister(userId);
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -70,7 +73,7 @@ client.defineJob({
|
||||
|
||||
```typescript
|
||||
//1. create a DynamicTrigger
|
||||
const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, {
|
||||
const dynamicOnIssueOpenedTrigger = client.defineDynamicTrigger({
|
||||
id: "github-issue-opened",
|
||||
event: events.onIssueOpened,
|
||||
source: github.sources.repo,
|
||||
@@ -96,7 +99,7 @@ client.defineJob({
|
||||
//3. Register the DynamicTrigger anywhere in your app
|
||||
async function registerRepo(owner: string, repo: string) {
|
||||
//the first param (key) should be unique
|
||||
await dynamicOnIssueOpenedTrigger.register(`${owner}/${repo}`, {
|
||||
await dynamicOnIssueOpenedTrigger.register(`${owner}-${repo}`, {
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
@@ -114,15 +117,10 @@ client.defineJob({
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//6. Register the dynamic trigger so you get notified when an issue is opened
|
||||
return await io.registerTrigger(
|
||||
"register-repo",
|
||||
dynamicOnIssueOpenedTrigger,
|
||||
payload.repository.name,
|
||||
{
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
}
|
||||
);
|
||||
await dynamicOnIssueOpenedTrigger.register(`${owner}-${repo}`, {
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -22,19 +22,15 @@ To begin, install the necessary packages in your Next.js project directory. You
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
|
||||
npm i @trigger.dev/sdk @trigger-dev/nextjs
|
||||
|
||||
npm i @trigger.dev/sdk @trigger.dev/nextjs
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger-dev/nextjs
|
||||
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/nextjs
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger-dev/nextjs
|
||||
|
||||
yarn add @trigger.dev/sdk @trigger.dev/nextjs
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -58,7 +54,7 @@ Create a `.env.local` file at the root of your project and include your Trigger
|
||||
```bash
|
||||
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://cloud.trigger.dev
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
|
||||
```
|
||||
|
||||
@@ -66,59 +62,23 @@ Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained f
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
To set up the Trigger Client for your project, follow these steps:
|
||||
Create a file at `<root>/src/trigger.ts` or `<root>/trigger.ts` depending on whether you're using the `src` directory or not. `<root>` represents the root directory of your project.
|
||||
|
||||
1. **Create Configuration File:**
|
||||
Next, add the following code to the file which creates and exports a new `TriggerClient`:
|
||||
|
||||
In your project directory, create a configuration file named `trigger.ts` or `trigger.js`, depending on whether your project uses TypeScript (`.ts`) or JavaScript (`.js`).
|
||||
```typescript src/trigger.(ts/js)
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
2. **Choose Directory:**
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
Depending on your project structure, choose the appropriate directory for the configuration file. If your project uses a `src` directory, create the file within it. Otherwise, create it directly in the project root.
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add Configuration Code:**
|
||||
|
||||
Open the configuration file you created and add the following code:
|
||||
|
||||
```typescript
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
|
||||
|
||||
4. **File Location:**
|
||||
|
||||
Depending on your project structure, save the configuration file in the appropriate location:
|
||||
|
||||
- If your project uses a **src** directory, save the file within the **src** directory.
|
||||
- If your project does not use a **src** directory, save the file in the project root.
|
||||
|
||||
**Example Directory Structure with src:**
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── src/
|
||||
├── trigger.ts
|
||||
├── other files...
|
||||
```
|
||||
|
||||
**Example Directory Structure without src:**
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── trigger.ts
|
||||
├── other files...
|
||||
```
|
||||
|
||||
By following these steps, you'll configure the Trigger Client to work with your project, regardless of whether you have a separate **src** directory and whether you're using TypeScript or JavaScript files.
|
||||
Replace **"my-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Creating the API Route
|
||||
|
||||
@@ -238,18 +198,31 @@ Your `package.json` file might look something like this:
|
||||
|
||||
Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client.
|
||||
|
||||
## Next Steps
|
||||
## Running
|
||||
|
||||
Start your Next.js project locally, and then execute the `dev` CLI command to run Trigger.dev locally. You should run this command every time you want to use Trigger.dev locally.
|
||||
### Run your Next.js app
|
||||
|
||||

|
||||
Run your Next.js app locally, like you normally would. For example:
|
||||
|
||||
<Warning>
|
||||
Make sure your Next.js site is running locally before continuing. You must also leave this `dev`
|
||||
terminal command running while you develop.
|
||||
</Warning>
|
||||
<CodeGroup>
|
||||
|
||||
In a **new terminal window or tab** run:
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Run the CLI 'dev' command
|
||||
|
||||
In a **_separate terminal window or tab_** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
@@ -271,9 +244,10 @@ yarn dlx @trigger.dev/cli@latest dev
|
||||
You can optionally pass the port if you're not running on 3000 by adding
|
||||
`--port 3001` to the end
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
You can optionally pass the hostname if you're not running on localhost by adding
|
||||
`--hostname <host>`. Example, in case your Next.js is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
`--hostname <host>`. Example, in case your Remix is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
|
||||
@@ -4,4 +4,16 @@ sidebarTitle: "Astro"
|
||||
description: "How to get setup and deploy Jobs for your Astro project"
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-astro.mdx" />
|
||||
<Snippet file="long-running-coming-soon.mdx" />
|
||||
|
||||
## Initial setup
|
||||
|
||||
View our [Quick start guide](/documentation/quickstarts/astro) to get setup.
|
||||
|
||||
## Writing Jobs
|
||||
|
||||
View our [guide for writing Jobs](/documentation/guides/create-a-job).
|
||||
|
||||
## Deployment
|
||||
|
||||
View our [deployment guide](/documentation/guides/deployment) to learn how to deploy your Jobs.
|
||||
|
||||
@@ -3,15 +3,11 @@ title: "Next.js"
|
||||
description: "How to get setup and deploy Jobs for your Next.js project"
|
||||
---
|
||||
|
||||
You can write Jobs in your Next.js codebase and deploy to both serverless platforms and long-running servers.
|
||||
|
||||
## Supported platforms
|
||||
|
||||
We support all platforms because under-the-hood an API endpoint is used to run your Jobs.
|
||||
<Snippet file="long-running-coming-soon.mdx" />
|
||||
|
||||
## Initial setup
|
||||
|
||||
View our [Quick start guide](/documentation/quickstart) to get setup.
|
||||
View our [Quick start guide](/documentation/quickstarts/nextjs) to get setup. Or [manual setup](/documentation/guides/manual/nextjs) if you'd prefer.
|
||||
|
||||
## Writing Jobs
|
||||
|
||||
@@ -93,7 +89,6 @@ export const config = {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### Known issues
|
||||
|
||||
* ` Module parse failed: Identifier 'NextResponse' has already been declared . ` This error is caused by version 13.4.4. For further information, please [visit](https://github.com/nextauthjs/next-auth/issues/7660 )
|
||||
- `Module parse failed: Identifier 'NextResponse' has already been declared .` This error is caused by version 13.4.4. For further information, please [visit](https://github.com/nextauthjs/next-auth/issues/7660)
|
||||
|
||||
@@ -3,4 +3,16 @@ title: "Remix"
|
||||
description: "How to get setup and deploy Jobs for your Remix project"
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-remix.mdx" />
|
||||
<Snippet file="long-running-coming-soon.mdx" />
|
||||
|
||||
## Initial setup
|
||||
|
||||
View our [Quick start guide](/documentation/quickstarts/remix) to get setup.
|
||||
|
||||
## Writing Jobs
|
||||
|
||||
View our [guide for writing Jobs](/documentation/guides/create-a-job).
|
||||
|
||||
## Deployment
|
||||
|
||||
View our [deployment guide](/documentation/guides/deployment) to learn how to deploy your Jobs.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: "Automatic React hooks"
|
||||
description: "These allow you to show Run and Task progress without adding extra code to your Jobs."
|
||||
---
|
||||
|
||||
## The data you can receive
|
||||
|
||||
- Info about an event you sent, including the Runs it triggered.
|
||||
- The overall status of the Run (in progress, success and fail statuses).
|
||||
- Metadata like start and completed times.
|
||||
- The Run output (what is returned or an error that failed the Job)
|
||||
- Information about the Tasks that have completed/failed/are running.
|
||||
|
||||
## The hooks
|
||||
|
||||
- [useEventDetails](/sdk/react/useeventdetails): get the details of a specific event
|
||||
- [useRunDetails](/sdk/react/userundetails): get the details of a specific Run
|
||||
- [useEventRunDetails](/sdk/react/useeventrundetails): get the details of a Run triggered from a specific event
|
||||
|
||||
All of these hooks will automatically refresh your components as the state of your Runs or events change.
|
||||
|
||||
#### useEventDetails
|
||||
|
||||
The `useEventDetails` hook will get the details of a specific event. You can use this to show the status of a specific event.
|
||||
|
||||
<Snippet file="how-to-get-event-id.mdx" />
|
||||
|
||||
This component will show the details of an event and the overall status of Runs that were triggered by the event:
|
||||
|
||||
```tsx
|
||||
import { useEventDetails } from "@trigger.dev/react";
|
||||
|
||||
export default function EventDetails({ eventId }: { eventId: string }) {
|
||||
const { data, error } = useEventDetails(eventId);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.name}</h1>
|
||||
<p>Runs: {data.runs?.length}</p>
|
||||
<div>
|
||||
{data.runs?.map((run) => (
|
||||
<div key={run.id}>
|
||||
<p>
|
||||
Run {run.id}: {run.status}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### useRunDetails
|
||||
|
||||
The `useRunDetails` hook will get the details of a specific Run. You can use this to show the status of a specific Run.
|
||||
|
||||
<Snippet file="how-to-get-run-id.mdx" />
|
||||
|
||||
This component will show the details of a Run and the status of each task in the Run:
|
||||
|
||||
```tsx
|
||||
import { useRunDetails } from "@trigger.dev/react";
|
||||
|
||||
export default function RunDetails({ runId }: { runId: string }) {
|
||||
const { data, error } = useRunDetails(runId);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Run {data.id}</h1>
|
||||
<p>Status: {data.status}</p>
|
||||
<div>
|
||||
{data.tasks?.map((task) => (
|
||||
<div key={task.id}>
|
||||
<p>
|
||||
Task {task.id}: {task.status}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### useEventRunDetails
|
||||
|
||||
The `useEventRunDetails` hook will get the details of a specific Run that was triggered by a specific event. You can use this to show the status of a specific Run.
|
||||
|
||||
<Snippet file="how-to-get-event-id.mdx" />
|
||||
|
||||
This component will show the details of a Run and the status of each task in the Run:
|
||||
|
||||
```tsx
|
||||
import { useEventRunDetails } from "@trigger.dev/react";
|
||||
|
||||
export default function EventRunDetails({ eventId }: { eventId: string }) {
|
||||
const { data, error } = useEventRunDetails(eventId);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Run {data.id}</h1>
|
||||
<p>Status: {data.status}</p>
|
||||
<div>
|
||||
{data.tasks?.map((task) => (
|
||||
<div key={task.id}>
|
||||
<p>
|
||||
Task {task.id}: {task.status}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: "Explicit status hooks"
|
||||
description: "How to add `statuses` to your Job code and then subscribe using the hooks"
|
||||
---
|
||||
|
||||
You can create `statuses` in your Job code. This gives you fine grained control over what you want to expose.
|
||||
|
||||
It allows you to:
|
||||
|
||||
- Show exactly what you want in your UI (with as many statuses as you want).
|
||||
- Pass arbitrary data to your UI, which you can use to render elements.
|
||||
- Update existing elements in your UI as the progress of the run continues.
|
||||
|
||||
## In your Job code
|
||||
|
||||
You should create a "status", which you can then update throughout the Job if you'd like to. You can create more than one status. Each of these will come through to the hook with the latest status and the history for each.
|
||||
|
||||
```ts jobs/yourjob.ts
|
||||
//your job
|
||||
client.defineJob({
|
||||
id: "meme-generator",
|
||||
name: "Generate memes",
|
||||
version: "0.1.1",
|
||||
trigger: eventTrigger({
|
||||
name: "generate-memes",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//create a status "generating-memes"
|
||||
//you give it the starting state. Only label is required
|
||||
const generatingMemes = await io.createStatus("generating-memes", {
|
||||
//the label is compulsory on this first call
|
||||
label: "Generating memes",
|
||||
//state is optional
|
||||
state: "loading",
|
||||
//data is an optional object. the values can be any type that is JSON serializable
|
||||
data: {
|
||||
progress: 0.1,
|
||||
},
|
||||
});
|
||||
|
||||
//...do stuff, like generate memes
|
||||
|
||||
//update the generatingMemes status.
|
||||
//anything set here will override the previous values, but you'll be able to view the full history with hooks
|
||||
await generatingMemes.update("middle-generation", {
|
||||
//label isn't specified so will remain the same
|
||||
//state will be updated to "success"
|
||||
state: "success",
|
||||
//set data, this overrides the previous value
|
||||
data: {
|
||||
progress: 1,
|
||||
urls: [
|
||||
"https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZnZoMndsdWh0MmhvY2kyaDF6YjZjZzg1ZGsxdnhhYm13a3Q1Y3lkbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/13HgwGsXF0aiGY/giphy.gif",
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In this case we created just a single status `generating-memes` and then updated it. It's worth noting that you can create as many statuses as you'd like in a single Job and you can update them as many times as you want.
|
||||
|
||||
This allows you to fine-grained control over how you report progress and output data from your Job.
|
||||
|
||||
## Using the React hooks
|
||||
|
||||
There are two hooks you can use in your UI to display the Run statuses.
|
||||
|
||||
- **useEventRunStatuses**: get the statuses of a run that was triggered by a specified event
|
||||
- **useRunStatuses**: get the statuses of the specified run
|
||||
|
||||
## useEventRunStatuses
|
||||
|
||||
The `useEventRunStatuses` hook will give you the statuses and overview data of **the first run** that is triggered by an event.
|
||||
|
||||
<Snippet file="how-to-get-event-id.mdx" />
|
||||
|
||||
This component will show the details of a Run and the status of each task in the Run:
|
||||
|
||||
```ts
|
||||
import { useEventRunStatuses } from "@trigger.dev/react";
|
||||
|
||||
export function EventRunData({ id }: { id: string }) {
|
||||
const { fetchStatus, error, statuses, run } = useEventRunStatuses(id);
|
||||
|
||||
if (fetchStatus === "loading") {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (fetchStatus === "error") {
|
||||
return (
|
||||
<div>
|
||||
<p>{error.name}</p>
|
||||
<p>{error.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
//you receive the overall status of the run, e.g. SUCCESS, FAIL
|
||||
<div>Run status: {run.status}</div>
|
||||
<div>
|
||||
{statuses.map((status) => {
|
||||
switch (status.key) {
|
||||
case "generating-memes": {
|
||||
const urls = status.data?.urls as string[] | undefined;
|
||||
return (
|
||||
<div key={status.key}>
|
||||
// Will display: "Generating memes: loading"
|
||||
<p>
|
||||
{status.label}: {status.state}
|
||||
</p>
|
||||
//will render the memes as images
|
||||
{urls?.map((url) => <img key={url} src={url} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
//this is what's returned from the run function
|
||||
{run.output && (
|
||||
<code>
|
||||
<pre>{JSON.stringify(run.output, null, 2)}</pre>
|
||||
</code>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## useRunStatuses
|
||||
|
||||
The `useRunStatuses` hook will give you the statuses and overview data of a specific Run.
|
||||
|
||||
<Snippet file="how-to-get-run-id.mdx" />
|
||||
|
||||
This component will show the details of a Run and the status of each task in the Run:
|
||||
|
||||
```ts
|
||||
import { useRunStatuses } from "@trigger.dev/react";
|
||||
|
||||
export function RunData({ id }: { id: string }) {
|
||||
const { fetchStatus, error, statuses, run } = useRunStatuses(id);
|
||||
|
||||
if (fetchStatus === "loading") {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (fetchStatus === "error") {
|
||||
return (
|
||||
<div>
|
||||
<p>{error.name}</p>
|
||||
<p>{error.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
//you receive the overall status of the run, e.g. SUCCESS, FAIL
|
||||
<div>Run status: {run.status}</div>
|
||||
<div>
|
||||
{statuses.map((status) => {
|
||||
switch (status.key) {
|
||||
case "generating-memes": {
|
||||
const urls = status.data?.urls as string[] | undefined;
|
||||
return (
|
||||
<div key={status.key}>
|
||||
// Will display: "Generating memes: loading"
|
||||
<p>
|
||||
{status.label}: {status.state}
|
||||
</p>
|
||||
//will render the memes as images
|
||||
{urls?.map((url) => <img key={url} src={url} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
//this is what's returned from the run function
|
||||
{run.output && (
|
||||
<code>
|
||||
<pre>{JSON.stringify(run.output, null, 2)}</pre>
|
||||
</code>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "React hooks"
|
||||
title: "Overview"
|
||||
description: "How to show the live status of Job Runs in your React app"
|
||||
---
|
||||
|
||||
@@ -9,207 +9,168 @@ You can display the live progress of a Job Run to your users, including the stat
|
||||
<img src="/images/react-hooks.png" />
|
||||
</Frame>
|
||||
|
||||
## Steps
|
||||
## Setting up your project for hooks
|
||||
|
||||
This guide assumes that your project is already setup and you have a Job running. If not, you should follow the [quick start guide](/documentation/quickstart) first.
|
||||
|
||||
### 1. Install the package
|
||||
<Steps titleSize="h3">
|
||||
<Step title="Install the package">
|
||||
Add the `@trigger.dev/react` package to your project:
|
||||
|
||||
Add the `@trigger.dev/react` package to your project:
|
||||
<CodeGroup>
|
||||
|
||||
<CodeGroup>
|
||||
```bash npm
|
||||
npm install @trigger.dev/react@latest
|
||||
```
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/react@latest
|
||||
```
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/react@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/react@latest
|
||||
```
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/react@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/react@latest
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</CodeGroup>
|
||||
</Step>
|
||||
<Step title="Get your public API key">
|
||||
In the Trigger.dev dashboard you should go to your Project and then the "Environments & API Keys" page.
|
||||
|
||||
### 2. Get your public API key
|
||||

|
||||
|
||||
In the Trigger.dev dashboard you should go to your Project and then the "Environments & API Keys" page.
|
||||
You should copy the `PUBLIC` API key for the dev environment.
|
||||
|
||||

|
||||
<Accordion title="What's a public API key?">
|
||||
A public API key is a key that can be used in the browser. It can only be used to read certain
|
||||
data from the API and can not write data. This means that it can be used to get the status of a
|
||||
Job Run, but not to start a new Job Run.
|
||||
</Accordion>
|
||||
|
||||
You should copy the `PUBLIC` API key for the dev environment.
|
||||
</Step>
|
||||
<Step title="Setting up environment variables">
|
||||
<Tabs>
|
||||
<Tab title="Next.js">
|
||||
Add the `NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
|
||||
<Accordion title="What's a public API key?">
|
||||
A public API key is a key that can be used in the browser. It can only be used to read certain
|
||||
data from the API and can not write data. This means that it can be used to get the status of a
|
||||
Job Run, but not to start a new Job Run.
|
||||
</Accordion>
|
||||
```sh .env.local
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
|
||||
### 3. Add the env var to your project
|
||||
Your private API key should already be in there.
|
||||
|
||||
Add the `NEXT_PUBLIC_TRIGGER_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
`NEXT_PUBLIC_` is a special prefix that exposes the environment variable to your users' web browsers.
|
||||
</Tab>
|
||||
|
||||
```sh .env.local
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
NEXT_PUBLIC_TRIGGER_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
<Tab title="Remix/React">
|
||||
Add the `TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
|
||||
Your private API key should already be in there.
|
||||
```sh .env
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
|
||||
### 4. Add the `TriggerProvider` component
|
||||
You will need to pass this value from the server to the client. We recommend you do this in your Root loader.
|
||||
|
||||
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
|
||||
</Tab>
|
||||
|
||||
Generally you'll want to add this to the root of your app, so that it's available everywhere. However, you can add it lower in the hierarchy but it must be above any of the hooks.
|
||||
</Tabs>
|
||||
|
||||
```tsx app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_API_KEY!}>
|
||||
{children}
|
||||
</TriggerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
### 5. Add hooks to your components
|
||||
<Step title="Add the <TriggerProvider> component">
|
||||
|
||||
There are three hooks that you can use to show statuses:
|
||||
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
|
||||
|
||||
- [useEventDetails](/sdk/react/useeventdetails): get the details of a specific event
|
||||
- [useRunDetails](/sdk/react/userundetails): get the details of a specific Run
|
||||
- [useEventRunDetails](/sdk/react/useeventrundetails): get the details of a Run triggered from a specific event
|
||||
Generally you'll want to add this to the root of your app, so that it's available everywhere. However, you can add it lower in the hierarchy but it must be above any of the hooks.
|
||||
|
||||
All of these hooks will automatically refresh your components as the state of your Runs or events change.
|
||||
<Tabs>
|
||||
<Tab title="Next.js">
|
||||
|
||||
#### 5.a useEventDetails
|
||||
```tsx app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY!}>
|
||||
{children}
|
||||
</TriggerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
The `useEventDetails` hook will get the details of a specific event. You can use this to show the status of a specific event.
|
||||
<Tab title="Remix">
|
||||
|
||||
<Snippet file="how-to-get-event-id.mdx" />
|
||||
```tsx app/root.tsx
|
||||
//return the public key env var from the loader so it's available in the browser
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
//...other code
|
||||
|
||||
This component will show the details of an event and the overall status of Runs that were triggered by the event:
|
||||
const triggerPublicApiKey = env.TRIGGER_PUBLIC_API_KEY!;
|
||||
|
||||
```tsx
|
||||
import { useEventDetails } from "@trigger.dev/react";
|
||||
return json({
|
||||
//...other data
|
||||
triggerPublicApiKey
|
||||
});
|
||||
}
|
||||
|
||||
export default function EventDetails({ eventId }: { eventId: string }) {
|
||||
const { data, error } = useEventDetails(eventId);
|
||||
//Your default export, i.e. the page component
|
||||
export default function App() {
|
||||
const {
|
||||
//...other data
|
||||
triggerPublicApiKey
|
||||
} = useLoaderData<typeof loader>();
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{/* wrap your outlet in this */}
|
||||
<TriggerProvider publicApiKey={triggerPublicApiKey}>
|
||||
<Outlet />
|
||||
</TriggerProvider>
|
||||
<ScrollRestoration />
|
||||
<ExternalScripts />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
</Tabs>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.name}</h1>
|
||||
<p>Runs: {data.runs?.length}</p>
|
||||
<div>
|
||||
{data.runs?.map((run) => (
|
||||
<div key={run.id}>
|
||||
<p>
|
||||
Run {run.id}: {run.status}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
#### 5.b useRunDetails
|
||||
## Two ways to report Run progress
|
||||
|
||||
The `useRunDetails` hook will get the details of a specific Run. You can use this to show the status of a specific Run.
|
||||
**Automatic progress** – without writing additional code in your Job you can get updates on the overall run status and individual tasks inside the run.
|
||||
|
||||
<Accordion title="How do I get a Run id?">
|
||||
You can call [client.getRuns()](/sdk/triggerclient/instancemethods/getruns) with a Job id to get a
|
||||
list of the most recent Runs for that Job. You can then pass that run id to your frontend to use
|
||||
in the hook.
|
||||
</Accordion>
|
||||
**Explicit status** – add code to your Job that reports the status of the things you're doing. This gives you full flexibility for displaying progress in your UI.
|
||||
|
||||
This component will show the details of a Run and the status of each task in the Run:
|
||||
|
||||
```tsx
|
||||
import { useRunDetails } from "@trigger.dev/react";
|
||||
|
||||
export default function RunDetails({ runId }: { runId: string }) {
|
||||
const { data, error } = useRunDetails(runId);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Run {data.id}</h1>
|
||||
<p>Status: {data.status}</p>
|
||||
<div>
|
||||
{data.tasks?.map((task) => (
|
||||
<div key={task.id}>
|
||||
<p>
|
||||
Task {task.id}: {task.status}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.c useEventRunDetails
|
||||
|
||||
The `useEventRunDetails` hook will get the details of a specific Run that was triggered by a specific event. You can use this to show the status of a specific Run.
|
||||
|
||||
<Snippet file="how-to-get-event-id.mdx" />
|
||||
|
||||
This component will show the details of a Run and the status of each task in the Run:
|
||||
|
||||
```tsx
|
||||
import { useEventRunDetails } from "@trigger.dev/react";
|
||||
|
||||
export default function EventRunDetails({ eventId }: { eventId: string }) {
|
||||
const { data, error } = useEventRunDetails(eventId);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Run {data.id}</h1>
|
||||
<p>Status: {data.status}</p>
|
||||
<div>
|
||||
{data.tasks?.map((task) => (
|
||||
<div key={task.id}>
|
||||
<p>
|
||||
Task {task.id}: {task.status}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Automatic hooks" icon="wrench" href="/documentation/guides/react-hooks-automatic">
|
||||
Receive coarse updates without writing additional Job code
|
||||
</Card>
|
||||
<Card
|
||||
title="Explicit status hooks"
|
||||
icon="wrench"
|
||||
href="/documentation/guides/react-hooks-statuses"
|
||||
>
|
||||
Add statuses to your Job code for fine-grained UI updates
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: "API Keys and Personal Access Tokens"
|
||||
description: "Lots of APIs use API Keys or Personal Access Tokens to authenticate. This guide will show you how to use them."
|
||||
sidebarTitle: "API Keys and PATs"
|
||||
---
|
||||
|
||||
## 1. Create an Integration client
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
---
|
||||
title: "Bring Your Own Auth"
|
||||
description: "Use Auth Resolvers to provide custom authentication credentials"
|
||||
---
|
||||
|
||||
In the previous guides we've covered how you can use our integrations with [API Keys](/documentation/guides/using-integrations-apikeys) or [OAuth](/documentation/guides/using-integrations-oauth), but in both cases those authentication credentials belong **to you** the developer.
|
||||
|
||||
If you want to use our integrations using auth credentials of **your users** you can use an Auth Resolver which allows you to implement your own custom auth resolving using a third-party service like [Clerk](https://clerk.com/) or [Nango](https://www.nango.dev/)
|
||||
|
||||
In this guide we'll demonstrate how to use Clerk.com's [Social Connections](https://clerk.com/docs/authentication/social-connections/oauth) to allow you to make requests with your user's Slack credentials and the official Trigger.dev [Slack integration](/integrations/apis/slack)
|
||||
|
||||
<Note>
|
||||
We won't be covering how to setup Clerk.com and their Social Connections to get the auth. This
|
||||
guide assumes you already have all that setup.
|
||||
</Note>
|
||||
|
||||
## 1. Install the Slack integration package
|
||||
|
||||
<Snippet file="installs/slack.mdx" />
|
||||
|
||||
## 2. Create a Slack integration
|
||||
|
||||
```ts slack.ts
|
||||
import { Slack } from "@trigger.dev/slack";
|
||||
|
||||
const byoSlack = new Slack({
|
||||
id: "byo-slack",
|
||||
});
|
||||
```
|
||||
|
||||
## 3. Define an Auth Resolver
|
||||
|
||||
Using your `TriggerClient` instance, define a new Auth Resolver for the `slack` integration:
|
||||
|
||||
```ts slack.ts
|
||||
import { Slack } from "@trigger.dev/slack";
|
||||
// Import your TriggerClient instance. This is merely an example of how you could do it
|
||||
import { client } from "./trigger";
|
||||
|
||||
const byoSlack = new Slack({
|
||||
id: "byo-slack",
|
||||
});
|
||||
|
||||
client.defineAuthResolver(byoSlack, async (ctx) => {
|
||||
// this is where we'll use the clerk backend SDK
|
||||
});
|
||||
```
|
||||
|
||||
## 4. Define a job
|
||||
|
||||
Before we finish the Slack Auth Resolver, let's create an example job that uses the Slack integration:
|
||||
|
||||
```ts slack.ts
|
||||
import { z } from "zod";
|
||||
|
||||
client.defineJob({
|
||||
id: "post-a-message",
|
||||
name: "Post a Slack Message",
|
||||
version: "1.0.0",
|
||||
trigger: eventTrigger({
|
||||
name: "post.message",
|
||||
schema: z.object({
|
||||
text: z.string(),
|
||||
channel: z.string(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
slack: byoSlack,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.slack.postMessage("💬", {
|
||||
channel: payload.channel,
|
||||
text: payload.text,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
As you can see above, we're passing the `byoSlack` integration into the Job and using it by calling `io.slack.postMessage`.
|
||||
|
||||
## 5. Install the Clerk backend SDK
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @clerk/backend@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @clerk/backend@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @clerk/backend@latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## 6. Import and initialize the Clerk SDK
|
||||
|
||||
```ts slack.ts
|
||||
import { Clerk } from "@clerk/backend";
|
||||
|
||||
// Clerk is not a class so the omission of `new Clerk` here is on purpose
|
||||
const clerk = Clerk({ apiKey: process.env.CLERK_API_KEY });
|
||||
```
|
||||
|
||||
## 7. Implement the Auth Resolver
|
||||
|
||||
Now we'll implement the Auth Resolver to provide authentication credentials saved in Clerk.com for Job runs, depending on the account ID of the run.
|
||||
|
||||
```ts slack.ts
|
||||
client.defineAuthResolver(slack, async (ctx) => {
|
||||
if (!ctx.account?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokens = await clerk.users.getUserOauthAccessToken(ctx.account.id, "oauth_slack");
|
||||
|
||||
if (tokens.length === 0) {
|
||||
throw new Error(`Could not find Slack auth for account ${ctx.account.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
token: tokens[0].token,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
The first parameter to the Auth Resolver callback is the run context ([reference docs](/sdk/context)), which optionally contains an associated account (more on this below).
|
||||
|
||||
<Warning>
|
||||
If the Auth Resolver returns undefined or throws an Error, any Job Run that uses the `byoSlack`
|
||||
integration will fail with an "Unresolved auth" error.
|
||||
</Warning>
|
||||
|
||||
## Bonus: Multiple Slack integration clients
|
||||
|
||||
If you want to also use Slack with your own authentication credentials, you can always create _another_ slack integration with a different `id`.
|
||||
|
||||
```ts slack.ts
|
||||
const ourSlack = new Slack({ id: "our-slack" });
|
||||
|
||||
client.defineJob({
|
||||
id: "post-a-message",
|
||||
name: "Post a Slack Message",
|
||||
version: "1.0.0",
|
||||
trigger: eventTrigger({
|
||||
name: "post.message",
|
||||
schema: z.object({
|
||||
text: z.string(),
|
||||
channel: z.string(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
byoSlack: byoSlack,
|
||||
ourSlack: ourSlack,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.byoSlack.postMessage("💬", {
|
||||
channel: payload.channel,
|
||||
text: payload.text,
|
||||
});
|
||||
|
||||
await io.ourSlack.postMessage("📢", {
|
||||
channel: "C01234567",
|
||||
text: `We just sent the following message to ${ctx.account?.id}: ${payload.text}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
# How to Trigger Job runs with an Account ID
|
||||
|
||||
Now that we have a working Clerk.com Auth Resolver for Slack we're ready to start triggering jobs with an associated account ID. The way you do this is different depending on the Trigger type.
|
||||
|
||||
## Event Triggers
|
||||
|
||||
Jobs that have [Event Triggers](/documentation/concepts/triggers/events) can be run with an associated account by providing an `accountId` when calling `sendEvent`:
|
||||
|
||||
```ts backend.ts
|
||||
// This is an instance of `TriggerClient`
|
||||
await client.sendEvent(
|
||||
{
|
||||
name: "post.created",
|
||||
payload: { id: "post_123" },
|
||||
},
|
||||
{
|
||||
accountId: "user_123",
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
The `accountId` value is completely arbitrary and doesn't map to anything inside Trigger.dev, but generally it should be a unique ID that can be used to lookup Auth credentials in your Auth Resolvers.
|
||||
|
||||
You can also send events with an associated account ID from the run of another job:
|
||||
|
||||
```ts anotherJob.ts
|
||||
client.defineJob({
|
||||
id: "event-1",
|
||||
name: "Run when the foo.bar event happens",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "foo.bar",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//send an event using `io`
|
||||
await io.sendEvent(
|
||||
"🎫",
|
||||
{
|
||||
name: "post.created",
|
||||
payload: { id: "post_123" },
|
||||
},
|
||||
{
|
||||
accountId: "user_123",
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When a run is triggered with an associated account ID, you'll see the account ID in the run dashboard:
|
||||
|
||||

|
||||
|
||||
## Scheduled Triggers
|
||||
|
||||
Running a job with an associated account ID that is triggered by a [Scheduled Trigger](/documentation/concepts/triggers/scheduled) works a bit differently than Event Triggers as you'll need to convert your normal `intervalTrigger` or `cronTrigger` into using a [Dynamic Schedule](/documentation/concepts/triggers/dynamic#dynamicschedule) and then registering schedules with an associated account ID.
|
||||
|
||||
### 1. Convert a job to using a Dynamic Schedule
|
||||
|
||||
First let's convert the following job from an `intervalTrigger` to a Dynamic Schedule:
|
||||
|
||||
```ts dynamicSchedule.ts
|
||||
// Before
|
||||
client.defineJob({
|
||||
id: "scheduled-job",
|
||||
name: "Scheduled Job",
|
||||
version: "1.0.0",
|
||||
trigger: intervalTrigger({
|
||||
seconds: 60,
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This runs every 60 seconds");
|
||||
},
|
||||
});
|
||||
|
||||
// After
|
||||
export const dynamicInterval = client.defineDynamicSchedule({ id: "my-schedule" });
|
||||
|
||||
client.defineJob({
|
||||
id: "scheduled-job",
|
||||
name: "Scheduled Job",
|
||||
version: "1.0.0",
|
||||
trigger: dynamicInterval,
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This runs dynamic schedules");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
As you can see above, we've dropped the specific interval when defining the trigger as that will now be specific when registering schedules.
|
||||
|
||||
### 2. Register a schedule
|
||||
|
||||
You can now use the `dynamicInterval` instance to register a schedule, which will trigger the `scheduled-job`:
|
||||
|
||||
```ts backend.ts
|
||||
import { dynamicInterval } from "./dynamicSchedule";
|
||||
|
||||
// Somewhere in your backend
|
||||
await dynamicInterval.register("schedule_123", {
|
||||
type: "interval",
|
||||
options: { seconds: 60 },
|
||||
accountId: "user_123", // associate runs triggered by this schedule with user_123
|
||||
});
|
||||
```
|
||||
|
||||
As you can see above, we've associated this registered schedule with an `accountId`, so any runs triggered by this schedule will be associated with `"user_123"`
|
||||
|
||||
The first parameter above `"schedule_123"` is the Schedule ID and can be used to unregister the schedule at a later point:
|
||||
|
||||
```ts backend.ts
|
||||
import { dynamicInterval } from "./dynamicSchedule";
|
||||
|
||||
// Somewhere in your backend
|
||||
await dynamicInterval.unregister("schedule_123");
|
||||
```
|
||||
|
||||
You can also use register/unregister inside another job run and it will automatically create a [Task](/documentation/concepts/tasks):
|
||||
|
||||
```ts otherJob.ts
|
||||
import { dynamicInterval } from "./dynamicSchedule";
|
||||
|
||||
client.defineJob({
|
||||
id: "event-1",
|
||||
name: "Run when the foo.bar event happens",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "foo.bar",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await dynamicInterval.register("schedule_123", {
|
||||
type: "interval",
|
||||
options: { seconds: 60 },
|
||||
accountId: "user_123", // associate runs triggered by this schedule with user_123
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Will produce the following run dashboard:
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
If you will only ever add a single schedule for a user on a given Dynamic Schedule, you can just
|
||||
use the accountId as the Schedule ID
|
||||
|
||||
```ts
|
||||
const accountId = "user_123";
|
||||
await dynamicInterval.register(accountId, {
|
||||
type: "interval",
|
||||
options: { seconds: 60 },
|
||||
accountId,
|
||||
});
|
||||
```
|
||||
|
||||
</Tip>
|
||||
|
||||
## Webhook Triggers
|
||||
|
||||
Running a job with an associated account ID that is triggered by a [Webhook Trigger](/documentation/concepts/triggers/webhook) requires converting to the use of a [Dynamic Trigger](/documentation/concepts/triggers/dynamic#dynamictrigger)
|
||||
|
||||
Dynamic Trigger's work very similarly to Dynamic Schedules, but instead of registering schedules, you register triggers:
|
||||
|
||||
<Steps>
|
||||
<Step title="Create Dynamic Trigger">
|
||||
|
||||
Using the GitHub integration we'll create a Dynamic Trigger that is triggered by the `onIssueOpened` event:
|
||||
|
||||
```ts github.ts
|
||||
import { Github, events } from "@trigger.dev/github";
|
||||
|
||||
const github = new Github({
|
||||
id: "github",
|
||||
});
|
||||
|
||||
const dynamicOnIssueOpenedTrigger = client.defineDynamicTrigger({
|
||||
id: "github-issue-opened",
|
||||
event: events.onIssueOpened,
|
||||
source: github.sources.repo,
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Use the Dynamic Trigger">
|
||||
|
||||
Now we'll use the Dynamic Trigger to define a Job that is triggered by it:
|
||||
|
||||
```ts github.ts
|
||||
client.defineJob({
|
||||
id: "listen-for-dynamic-trigger",
|
||||
name: "Listen for dynamic trigger",
|
||||
version: "0.1.1",
|
||||
trigger: dynamicOnIssueOpenedTrigger,
|
||||
integrations: {
|
||||
github,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.github.issues.createComment("create-issue-comment", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
issueNumber: payload.issue.number,
|
||||
body: "First! 🥇",
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Define Auth Resolver">
|
||||
|
||||
Define an Auth Resolver to fetch the GitHub OAuth token from Clerk.com:
|
||||
|
||||
```ts github.ts
|
||||
client.defineAuthResolver(github, async (ctx) => {
|
||||
if (!ctx.account?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokens = await clerk.users.getUserOauthAccessToken(ctx.account.id, "oauth_github");
|
||||
|
||||
if (tokens.length === 0) {
|
||||
throw new Error(`Could not find GitHub auth for account ${ctx.account.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
token: tokens[0].token,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
If you are using clerk, you'll probably want to [Add additional
|
||||
scopes](https://clerk.com/docs/authentication/social-connections/oauth#request-additional-o-auth-scopes-after-sign-up)
|
||||
to be able to do useful things with the GitHub integration. For example, if you plan on
|
||||
registering GitHub triggers you'll need `write:repo_hook` and `read:repo_hook` or just
|
||||
`admin:repo_hook`. If you want to create issues you'll need `repo` or `public_repo`.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Register a new trigger">
|
||||
|
||||
Finally, we can register a new Trigger at "runtime", either inside another Job run or in your backend:
|
||||
|
||||
```ts github.ts
|
||||
// Register inside another job run:
|
||||
client.defineJob({
|
||||
id: "register-issue-opened",
|
||||
name: "Register Issue Opened for Account",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "register.issue.opened",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This will automatically create a task in this run with the `payload.id` as the Task Key.
|
||||
await dynamicOnIssueOpenedTrigger.register(
|
||||
payload.id,
|
||||
{
|
||||
owner: payload.owner,
|
||||
repo: payload.repo,
|
||||
},
|
||||
{
|
||||
accountId: payload.accountId,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Register in your backend:
|
||||
// This skips creating a Task since it's outside a job and will just call our backend API directly
|
||||
async function registerIssueOpenedTrigger(
|
||||
id: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
accountId?: string
|
||||
) {
|
||||
return await dynamicOnIssueOpenedTrigger.register(
|
||||
id,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
},
|
||||
{
|
||||
accountId,
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
# Testing jobs with Account ID
|
||||
|
||||
If a job uses any integrations with an Auth Resolver that requires an account ID, you'll need to provide an account ID when testing the job:
|
||||
|
||||

|
||||
|
||||
# Auth Resolver reference
|
||||
|
||||
The Auth Resolver callback has the following signature:
|
||||
|
||||
```ts
|
||||
type TriggerAuthResolver = (
|
||||
ctx: TriggerContext,
|
||||
integration: TriggerIntegration
|
||||
) => Promise<AuthResolverResult | undefined>;
|
||||
|
||||
type AuthResolverResult = {
|
||||
type: "apiKey" | "oauth";
|
||||
token: string;
|
||||
additionalFields?: Record<string, string>;
|
||||
};
|
||||
```
|
||||
|
||||
The `ctx` parameter is the [TriggerContext](/sdk/context) for the run and the `integration` parameter is the [TriggerIntegration](/sdk/integrations) instance that the Auth Resolver is being called for. You can use the `integration` parameter to check the `id` of the integration to determine which integration the Auth Resolver is being called for:
|
||||
|
||||
```ts
|
||||
client.defineAuthResolver(slack, async (ctx, integration) => {
|
||||
if (integration.id === "byo-slack") {
|
||||
// do something
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can also return `additionalFields` in the Auth Resolver result which will be passed to the integration when making requests. This is useful if you need to provide additional fields to the integration that are not part of the standard integration options.
|
||||
|
||||
```ts
|
||||
client.defineAuthResolver(shopify, async (ctx, integration) => {
|
||||
return {
|
||||
type: "apiKey",
|
||||
token: "my-api-key",
|
||||
additionalFields: {
|
||||
shop: "my-shop-name",
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Using Integrations"
|
||||
description: "How to use Integrations"
|
||||
title: "Integrations Overview"
|
||||
description: "How to use Trigger.dev Integrations"
|
||||
sidebarTitle: "Overview"
|
||||
---
|
||||
|
||||
<Note>
|
||||
You can use any API in your Jobs by using existing Node.js SDKs or HTTP
|
||||
requests. Integrations just make it much easier especially when you want to
|
||||
use OAuth. And you get great logging.
|
||||
You can use any API in your Jobs by using existing Node.js SDKs or HTTP requests. Integrations
|
||||
just make it much easier especially when you want to use OAuth. And you get great logging.
|
||||
</Note>
|
||||
|
||||
[Integrations](/documentation/concepts/integrations) allow you to quickly use APIs, including webhooks and Tasks.
|
||||
@@ -37,6 +37,14 @@ There are two ways to authenticate Integrations, OAuth and API Keys/Access Token
|
||||
>
|
||||
Use OAuth to connect an Integration for your team or your users
|
||||
</Card>
|
||||
<Card
|
||||
title="Bring-your-own Auth"
|
||||
icon="user"
|
||||
href="/documentation/guides/using-integrations-byo-auth"
|
||||
>
|
||||
Use our integrations with your user’s auth credentials, using Clerk.com, Nango.dev, or rolling
|
||||
your own with our custom auth resolvers
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Using for Jobs & Tasks
|
||||
@@ -121,7 +129,7 @@ import { Stripe } from "@trigger.dev/stripe";
|
||||
|
||||
const stripe = new Stripe({
|
||||
id: "stripe",
|
||||
apiKey: process.env.STRIPE_SECRET_KEY!
|
||||
apiKey: process.env.STRIPE_SECRET_KEY!,
|
||||
});
|
||||
|
||||
async function createCustomer() {
|
||||
@@ -161,7 +169,6 @@ client.defineJob({
|
||||
Behind the scenes, our `@trigger.dev/github` integration will create a webhook on your repository that will call our API when a new push event is received. We will then start your Job with the payload from the push event.
|
||||
|
||||
<Note>
|
||||
If you are just using an integration to trigger a job but not using
|
||||
authenticated tasks inside the job run, there is no need to pass the
|
||||
integration in the job `integrations` option.
|
||||
If you are just using an integration to trigger a job but not using authenticated tasks inside the
|
||||
job run, there is no need to pass the integration in the job `integrations` option.
|
||||
</Note>
|
||||
|
||||
@@ -59,7 +59,11 @@ We'd love to hear from you or give you a hand getting started. Here are some way
|
||||
>
|
||||
Follow us on Twitter to get the latest updates and news.
|
||||
</Card>
|
||||
<Card title="Schedule a call" icon="phone" href="https://cal.com/team/triggerdotdev/support">
|
||||
<Card
|
||||
title="Schedule a call"
|
||||
icon="phone"
|
||||
href="https://cal.com/team/triggerdotdev/founders-call"
|
||||
>
|
||||
Arrange a call with one of the founders. We can help answer questions, build API Integrations
|
||||
for you and give 1-on-1 help building your first Job.
|
||||
</Card>
|
||||
|
||||
@@ -4,4 +4,81 @@ sidebarTitle: "Astro"
|
||||
description: "Start creating Jobs in 5 minutes in your Astro project."
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-astro.mdx" />
|
||||
This quick start guide will get you up and running with Trigger.dev.
|
||||
|
||||
<Accordion title="Need to create a new Astro project to add Trigger.dev to?">
|
||||
No problem, create a blank project by running the `create-astro` command in your terminal then continue with this quickstart guide as normal:
|
||||
|
||||
```bash
|
||||
npx create-astro@latest
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Steps titleSize="h3">
|
||||
<Snippet file="quickstart-setup-steps.mdx" />
|
||||
|
||||
<Step title="Run the CLI `dev` command">
|
||||
|
||||
<Snippet file="quickstart-cli-dev.mdx" />
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Advanced: Run your Astro server together with the CLI">
|
||||
You can modify your `package.json` to run both the Astro server and the CLI `dev` command together.
|
||||
|
||||
1. Install the `concurrently` package:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add concurrently --dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
2. Modify your `package.json` file's `dev` script.
|
||||
|
||||
```json package.json
|
||||
//...
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others npm:dev:*",
|
||||
//your normal astro dev command would go here
|
||||
"dev:astro": "astro dev",
|
||||
"dev:trigger": "npx @trigger.dev/cli dev",
|
||||
//...
|
||||
}
|
||||
//...
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Your first job">
|
||||
|
||||
The CLI init command created a simple Job for you. There will be a new file `src/jobs/example.(ts/js)`.
|
||||
|
||||
In there is this Job:
|
||||
|
||||
<Snippet file="quickstart-example-job.mdx" />
|
||||
|
||||
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Snippet file="quickstart-running-your-job.mdx" />
|
||||
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user