Compare commits

...

62 Commits

Author SHA1 Message Date
Matt Aitken bc757c8ddb Latest lockfile 2023-09-27 18:12:54 +01:00
github-actions[bot] 6e11ab9183 chore: Update version for release (#513)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-27 18:09:16 +01:00
Matt Aitken 2397fcb640 Express frameworks docs + CLI (#512)
* Manual setup docs

* Added the onboarding

* The emails package now works with Node > 18

* Added CLi support for Express, including custom init command finished messages

* Need to actually log out the installation complete message…

* Fix for an old Remix reference

* Renamed the page export

* Use resolvedOptions.triggerUrl

* Typo in manual instructions
2023-09-27 17:44:50 +01:00
Eric Allam 35d0c2a06f Implement the task output redacting to prevent redacted values from showing in the logs 2023-09-27 16:38:30 +01:00
Matt Aitken 813ec74672 Increased the intervalTrigger max from 1 day to 30 days
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-26 17:23:36 +01:00
Matt Aitken 03db13171a Latest lockfile 2023-09-26 16:46:41 +01:00
Matt Aitken 0ecb5129e8 Fix for incorrectly named Next.js package in manual setup 2023-09-26 12:47:49 +01:00
github-actions[bot] 44cb28c1c4 chore: Update version for release (#508)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-26 11:49:03 +01:00
Matt Aitken 4578f6bd64 Astro CLI support (#506)
* Astro framework CLI support

* Changeset: Added Astro automatic installation

* Fixed the package name – was remix, now astro

* Fixed the export of the example

* Added IPv6 localhost to Astro hostnames

* Updated Astro onboarding to show the CLI init command, instead of manual instructions

* Astro quickstart

* Fix for type in Remix quickstart

* Next.js framework detection allows different config file extensions and “next” devDependency

* Made the dev command port more general so it works with various frameworks
2023-09-26 11:44:35 +01:00
Eric Allam 8b25e57613 hotfix 2 2023-09-24 14:43:02 -07:00
Eric Allam 8fb9ea19a3 hotfix 2023-09-24 14:04:46 -07:00
Eric Allam eb4ca0ce2d Fixed duplicate end month
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-22 17:24:55 -07:00
Eric Allam df24cd5b71 feat: Basic usage dashboard to show run volume (#501)
* New usage dashboard with static data

* Implement org usage dash

* Grab chart data for the last 12 months

* If no org is found just return undefined so a 404 will be shown

* Remove mock data

* Fill in missing months with 0s

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-09-22 17:02:53 -07:00
Matt Aitken 284324031c Latest lockfile 2023-09-22 16:23:21 -07:00
github-actions[bot] 2fdf42d444 chore: Update version for release (#481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-22 16:20:21 -07:00
Eric Allam 50137a6f24 Decouple zod (#500)
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
Zod Schemas is no longer required for validating/inferring event triggers. We’ve taken inspiration from how domain-functions did it: https://github.com/seasonedcc/domain-functions/pull/114
2023-09-22 14:44:03 -07:00
Matt Aitken 3703f989fa Remix onboarding now uses the CLI init command 2023-09-22 14:39:21 -07:00
Matt Aitken 2a42942b01 The CLI now checks for a dev server API key in init and dev commands 2023-09-22 13:37:59 -07:00
Matt Aitken 7b29a92f6a Removed dynamicTrigger @internal from toJSON 2023-09-22 12:24:07 -07:00
Eric Allam 24465c9a7b Add back in Job.toJSON to fix the testing package 2023-09-22 11:21:37 -07:00
Eric Allam 42022b6ba4 Add BYO auth for oauth options 2023-09-22 10:31:40 -07:00
Matt Aitken dcc807d716 Linear getAll type error (weirdly not in VSCode…) and removed the pagination example that uses the SDK as won’t work with timeouts 2023-09-22 10:23:24 -07:00
Matt Aitken b5e37bfa61 Fix for getPathAlias typecheck failure 2023-09-22 09:59:59 -07:00
Matt Aitken 5681ebc756 Latest lockfile 2023-09-22 09:53:30 -07:00
Matt Aitken 886d6fda36 Tweaked the Linear scopes 2023-09-22 09:51:49 -07:00
Matt Aitken 1a4952720f CLI now supports multiple frameworks (with tests) (#480)
* Early work defining CLI framework support

* WIP moving CLI init logic to the Framework class

* Installing files should now work for Nextjs

* Some fixes

* WIP creating unit tests for Next.js project detection

* Delete old jest config

* Latest lockfile

* Detect use of src directory test

* Tests for detection pages/app directory

* Correct detection of Next.js project

* Renamed test file

* Create install files from template files with replacements. With tests

* Added multiple uses of the same replacement

* Created a test for the install step (it fails right now with JS)

* Removed unused import

* Another test that should pass but currently fails…

* Path alias fixed and now has tests

* New pathAlias function used

* Nextjs page install tests

* Fixed app directory install (with tests)

* Removed e2e CLI test, switched to unit testing strategy instead

* Latest lockfile

* The install files are now actual files that are copied and transformed

* Got the template files working correctly after building

* Next steps are now framework specific

* createFileFromTemplate now works with a path again. Uses mock if specified.

* Renamed apiRoute.js to pagesApiRoute.js

* Simplified pages file generation

* Next.js app API route template

* Next.js App routing support, with common files logic shared

* Dev command now uses framework default values if they exist and aren’t overridden

* Unused import

* pathAlias now works for all frameworks

* Added a test to detect Next from the next.config.js

* WIP on Remix framework support

* Tests for Remix install

* Replaced references to Next.js

* Use a green ✔️ instead of  in the CLI

* Support for multiple hostnames

* Tunneling can now use the hostname and port

* Work on multiple ports

* Improved the error messages. Added some extra pots to Next.js

* Update the Remix templates to have .server in the imports

* Remix updated to use server-runtime instead of node. Node v18+

* Frameworks can specify the watch paths and ignore paths

* Define the watch variables above, so we can easily log them for debugging

* Don’t wait for outdated package checking when running the dev command

* Improved the Remix manual setup guide

* Rewriting docs for quickstart

* Updated the Next.js quickstart

* Remix quick start

* Added a changeset

* Improved the Next.js manual setup
2023-09-22 08:55:04 -07:00
Eric Allam c0dfa8048a feat: BYO Auth (#491)
* feat: BYO Auth

Define client-side auth resolvers to be able to supply custom authentication credentials for integrations before a run is performed

- Added new defineAuthResolver
- Update all integrations to support the new auth resolvers
- Strip internal symbols from .d.ts in integrations and trigger-sdk
- Added BYO Auth docs
- Update Dynamic Schedule to support associated account IDs
- Create external accounts just-in-time
- Added Account ID field to test job when there are external auth integrations
- Show Account ID on run dashboard
- Added new Run error state called “Unresolved auth”

* Added changeset

* Remove @internal from TriggerIntegration public methods

* Add void to the result union

* DynamicTriggers now work with the new BYO auth system, and added a bunch of docs and docs changes

* Add additional key material for registering dynamic trigger task

* Add new define* instance methods to the overview
2023-09-22 08:54:31 -07:00
nicktrn 3e63a7e7a0 fix: Fail client-side on invalid Stripe event names (#492)
* Parse event names

* Add changeset
2023-09-22 08:53:04 -07:00
Eric Allam 4cc690a6ce Update sendevent.mdx 2023-09-22 08:44:14 -07:00
nicktrn 537447e318 Use absolute image paths (#490) 2023-09-21 15:45:40 -07:00
nicktrn 15f17d27e0 Going exponential with Linear (#478)
* Unleash GPT magic

* Clean up after GPT

* All the hooks

* Provisional integration catalog entry

* Sample webhook jobs

* Attachments with alpha warnings

* Remove some verbose logs

* Fix IP restrictions

* Remove tunnel

* Revert "Remove tunnel"

This reverts commit c5b69ce6524e3b40c26b66cdc56e087b8576b8c6.

* Resolve event name clashes

* Remove circular dependency

* Use correct payload uuid

* Schema fixes

* Fix webhook event name

* Start to Linearify catalog entry

* Remove todo

* More catalog updates

* Make OAuth work

* Rename webhook helper

* Schema juggling

* More discrimination

* Add Issue SLA event

* Simplify triggers

* Handle rate limits

* Fix Project schema

* Payload examples

* Improve event props

* Remove redundant source metadata

* One type to rule them all

* Recursive WithoutFunctions type

* Linear output serializer

* Some tasks

* Update catalog entry

* Dynamic usage sample

* Bump version

* Remove tunnel

* More tasks

* Add optional skipRetrying on runTask errors

* Fail fast on user errors

* Entity getter tasks

* Another couple of tasks

* Token to apiKey

* Sort tasks

* Add filtered issue SLA triggers

* Add docs

* Type fixes

* Job catalog examples

* Serialization helper docs

* Add changeset

* Refactor webhooks

* Enhance properties

* Pagination helper and docs

* Clean up imports

* Change misc catalog job

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-09-21 15:14:54 -07:00
Matt Aitken 91fc1e80f3 Use the bell icon for the new status Tasks
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-21 15:01:35 -07:00
Gregory dfe680a390 Update introduction.mdx (#498)
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
Changed https://github.com/triggerdotdev/examples/tree/main/resend (which was a 404) to https://github.com/triggerdotdev/examples/tree/main/resend-email-form
2023-09-21 14:06:14 -07:00
Matt Aitken f73a2a424b Revert "Upgrade to the latest remix (pre v2)"
This reverts commit 4edc7112ab.
2023-09-21 13:19:03 -07:00
Matt Aitken 6ce87f169a Fixed dependency 2023-09-21 11:35:12 -07:00
Matt Aitken ad14983e21 React status hooks (#493)
* Added stripInternal to SDK tsconfig

* Statuses can now be set from a run, and are stored in the database

* Added the key to the returned status

* Made the test job have an extra step and only pass in some of the options

* client.getRunStatuses() and the corresponding endpoint

* client.getRun() now includes status info

* Fixed circular dependency schema

* Translate null to undefined

* Added the react package to the nextjs-reference tsconfig

* Removed unused OpenAI integration from nextjs-reference project

* New hooks for getting the statuses

* Disabled most of the nextjs-reference jobs

* Updated the hooks UI

* Updated the endpoints to deal with null statuses values

* The hook is working, with an example

* Changeset: “You can create statuses in your Jobs that can then be read using React hooks”

* Changeset config is back to the old changelog style

* WIP on new React hooks guide

* Guide docs for the new hooks

* Added the status hooks to the React hooks guide

* Removed the links to the status hooks reference for now

* Re-ordered the hooks

* Fix for an error in the docs

* Set a default of a blank array for the GetRunSchema
2023-09-21 10:24:37 -07:00
Eric Allam 4edc7112ab Upgrade to the latest remix (pre v2) 2023-09-18 22:20:41 +01:00
D-K-P c2ce707f3d Fixed cal.com link 2023-09-18 13:54:53 +01:00
Eric Allam cd70a29970 Improve the Astro manual setup guide 2023-09-15 17:40:36 +01:00
Eric Allam cd94d8fee2 Fixes broken pnpm lock file 2023-09-15 16:17:53 +01:00
Eric Allam 241e40e3e2 Add a references README 2023-09-15 16:12:50 +01:00
Eric Allam 486ed20ae7 Renamed the examples dir to references (true examples are in another repo and this was confusing) 2023-09-15 16:09:04 +01:00
James Ritchie e5ffc3a3f3 Added a link to the homepage from the side menu (#479)
* Added 2 new named icons

* New side menu link to the homepage

* Updated lock file

* Removed un-used import
2023-09-15 15:11:40 +01:00
Aniket Bindhani 363c74c6da Documentation Update: Added <github_username> instead of triggerdotdev to avoid confusion while cloning the repository (#477) 2023-09-15 12:55:56 +01:00
Eric Allam f98c425186 Redirect people to discord to ask a question 2023-09-15 12:54:54 +01:00
Matt Aitken da10ba907f Added instructions for how to do Changeset snapshots 2023-09-15 12:54:38 +01:00
Eric Allam 5aeef1233b A few teaks to the templates 2023-09-15 12:52:29 +01:00
Vishesh Rawal fb5f4e308f Created Temp for pull req, bugreport & feature req 2023-09-15 12:52:29 +01:00
Wesley 6ccb4fbc63 test/368/use vitest instead of jest (#470)
* chore: add vitest dependencies

* refactor: replace jest by vitest

* test: disable broken test

* Update pnpm-lock.yaml

* Update pnpm-lock.yaml

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-09-15 12:51:25 +01:00
Matt Aitken 88ebf4a1a0 Updated the Astro docs with SSR notes 2023-09-14 15:25:43 +01:00
Matt Aitken 9a0e6412e9 Bumped package versions to 2.1.3 2023-09-14 13:37:02 +01:00
Matt Aitken a68a912d56 Docs: improved the limitations 2023-09-14 11:55:51 +01:00
Matt Aitken dde51d6d25 Astro docs improvements 2023-09-14 11:55:38 +01:00
Matt Aitken dbd094216d Updated Astro setup docs: env import 2023-09-14 11:14:35 +01:00
Matt Aitken dc7f335beb Latest lockfile 2023-09-14 09:22:05 +01:00
github-actions[bot] 454cad69b4 chore: Update version for release (#473)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-13 18:07:22 +01:00
Matt Aitken f86117f92b Some improvements to the docs platforms section 2023-09-13 18:05:34 +01:00
Matt Aitken b4c67984bc Latest lockfile 2023-09-13 17:57:22 +01:00
Chigala 1b4db79885 Feat/astro support (#466)
* updated the @trigger.dev/astro package to support typescript

* set up a example astro project

* docs: manual setup of Trigger.dev in an Astro project

* doc: updated the manual setup docs for astro

* updated the webapp onboarding for astro

* cleaned up the example astro project

* removed next from dependency 😅

* updated the readme

* moved trigger.ts file and job folder to src directory

* created a .env.example file

* updated tsconfig

* updated to use aliases

* updated package.json

* updated the astro onboarding page

* updated the manual installation guide for astro

* updated to use alias

* added astro as  a dev dependency

* Remove extra comma I left in

* Changed Object.create() to a Record, so we have some type safety

* Added the localhost URL to the API in the Astro example

* Added the TRIGGER_API_URL to the trigger client in the example

* Added the CLI dev dependency to the Astro example

* Update the running instructions to be closer to Remix's

* Create friendly-carpets-collect.md

* Fix bad package name for Astro

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-09-13 17:55:33 +01:00
Matt Aitken e1e60499b2 Fix for incorrect remix package name in docs
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-13 15:41:24 +01:00
Matt Aitken 4eb2439d64 Don’t check for the page title in verify jobs e2e test 2023-09-13 13:57:35 +01:00
Matt Aitken 2895ad52fd Re-run the publish workflow when the test have been updated 2023-09-13 13:56:44 +01:00
512 changed files with 18342 additions and 6605 deletions
+1 -6
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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=
+38
View File
@@ -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.
+5
View File
@@ -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.
+12
View File
@@ -0,0 +1,12 @@
"📌 area: cli":
- any: ["cli/**/*"]
"📌 area: t3-app":
- any: ["cli/template/**/*"]
"📚 documentation":
- any: ["www/**/*"]
- any: ["**/*.md"]
"📌 area: ci":
- any: [".github/**/*"]
+27
View File
@@ -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]_
💯
+3 -2
View File
@@ -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
+9
View File
@@ -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
}
]
}
+9
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -106,7 +106,7 @@ export function TriggerDevStep() {
</Paragraph>
<TriggerDevCommand />
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
If youre 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,7 +51,7 @@ 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>
@@ -60,7 +60,7 @@ export function FrameworkSelector() {
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
<RedwoodLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupAstroPath(organization, project)}>
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
<AstroLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
@@ -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="Homepage"
icon="external-link"
to="https://trigger.dev"
isCollapsed={isCollapsed}
data-action="onboarding"
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)} />,
@@ -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">
+30 -16
View File
@@ -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";
@@ -76,6 +76,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({
@@ -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;
}
@@ -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>
);
@@ -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>
@@ -1,21 +1,109 @@
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="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">
Youll notice a new folder in your project called 'jobs'. Weve 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>
);
}
@@ -1,21 +1,116 @@
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
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 { 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="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>
);
}
@@ -25,7 +25,7 @@ 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";
export const handle: Handle = {
@@ -38,6 +38,8 @@ 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">
@@ -75,28 +77,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">
Youll notice a new folder in your project called 'jobs'. Weve 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 }));
}
}
+10 -2
View File
@@ -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;
}
@@ -41,13 +38,19 @@ export class IngestSendEvent {
this.#prismaClient,
async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.findUniqueOrThrow({
? await tx.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: options.accountId,
},
update: {},
})
: undefined;
@@ -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;
@@ -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 }
);
+71
View File
@@ -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;
}
}
+2 -2
View File
@@ -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",
+5
View File
@@ -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>
+15
View File
@@ -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>
+49
View File
@@ -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>
+220 -1
View File
@@ -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.
+193 -1
View File
@@ -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>
+16 -36
View File
@@ -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
+26
View File
@@ -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>
+26
View File
@@ -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:
![Test Job](/images/test-job.png)
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>
+80
View File
@@ -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.
![Go to the Environments & API Keys page ](/images/environments-link.png)
2. Copy the `DEV` **SERVER** API key.
![API Keys](/images/api-keys.png)
</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>
+20
View File
@@ -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>
+24
View File
@@ -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 |
+1 -1
View File
@@ -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,
});
},
});
```
+39 -65
View File
@@ -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
![Your first Job](/images/cli-dev.gif)
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>
+13 -1
View File
@@ -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)
+13 -1
View File
@@ -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>
)}
</>
);
}
```
+70 -181
View File
@@ -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,96 @@ 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
![Get your public API Key](/images/environments-public-apikey.png)
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.
![Get your public API Key](/images/environments-public-apikey.png)
<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="Add the env var to your project">
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.
<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_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.
</Step>
```sh .env.local
#...
TRIGGER_API_KEY=[your_private_api_key]
NEXT_PUBLIC_TRIGGER_API_KEY=[your_public_api_key]
#...
```
<Step title="Add the <TriggerProvider> component">
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
Your private API key should already be in there.
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.
### 4. Add the `TriggerProvider` component
```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>
);
}
```
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
</Step>
</Steps>
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.
## Two ways to report Run progress
```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>
);
}
```
**Automatic progress** without writing additional code in your Job you can get updates on the overall run status and individual tasks inside the run.
### 5. Add hooks to your components
**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.
There are three hooks that you can use to show statuses:
- [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.
#### 5.a 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>
);
}
```
#### 5.b useRunDetails
The `useRunDetails` hook will get the details of a specific Run. You can use this to show the status of a specific 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>
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:
![Event Trigger with Account ID](/images/byo-auth/run-dashboard-account-id.png)
## 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:
![Dynamic Schedule Task](/images/byo-auth/dynamic-schedule-task.png)
<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:
![Test Job with Account ID](/images/byo-auth/run-test-account-id.png)
# 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 users 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>
+5 -1
View File
@@ -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>
+78 -1
View File
@@ -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:
![Your first Job](/images/first-job.png)
</Step>
<Snippet file="quickstart-running-your-job.mdx" />
</Steps>
<Snippet file="quickstart-whats-next.mdx" />
@@ -3,10 +3,7 @@ title: "Introduction"
sidebarTitle: "Introduction"
---
<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>
<Snippet file="long-running-coming-soon.mdx" />
## Select a framework to get started…
+14 -157
View File
@@ -17,106 +17,12 @@ Trigger.dev works with either the Pages or App Router configuration.
</Accordion>
## Create a Trigger.dev account
<Steps titleSize="h3">
<Snippet file="quickstart-setup-steps.mdx" />
You can either:
<Step title="Run the CLI `dev` command">
- Use the [Trigger.dev Cloud](https://cloud.trigger.dev).
- Or [self-host](/documentation/guides/self-hosting) the service.
### 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.
### Getting an API key
1. Go to the "Environments & API Keys" page in your project.
![Go to the Environments & API Keys page ](/images/environments-link.png)
2. Copy the `DEV` **SERVER** API key.
![API Keys](/images/api-keys.png)
## Run the CLI `init` command
The easiest way to get started it to use the CLI. It will add Trigger.dev to your existing Next.js 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 few 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.
3. Enter a unique ID for your endpoint (you can just use the default by hitting enter)
## Run your Next.js site
Make sure your Next.js 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>
## Run the CLI `dev` command
The CLI `dev` command allows the Trigger.dev service to send messages to your Next.js 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 3000 by adding
`--port 3001` to the end
</Note>
<Snippet file="quickstart-cli-dev.mdx" />
<AccordionGroup>
<Accordion title="Experiencing an error? This could be due to middleware.">
@@ -152,7 +58,7 @@ yarn add concurrently --dev
"dev": "concurrently --kill-others npm:dev:*",
"dev:next": "next dev",
"dev:trigger": "npx @trigger.dev/cli dev",
...
//...
}
...
```
@@ -160,73 +66,24 @@ yarn add concurrently --dev
</Accordion>
</AccordionGroup>
## Your first job
</Step>
The CLI init command created a simple Job for you. There will be a new file either `api/trigger/route.ts` or `pages/api/trigger.ts`.
<Step title="Your first job">
The CLI init command created a simple Job for you. There will be a new file either `src/jobs/examples.(ts/js)` or `jobs/examples.(ts/js)`.
In there is this Job:
```typescript
//Job definition uses the client
client.defineJob({
// 1. Metadata
id: "example-job",
name: "Example Job",
version: "0.0.1",
// 2. Trigger
trigger: eventTrigger({
name: "example.event",
}),
// 3. Run function
run: async (payload, io, ctx) => {
// do something
await io.logger.info("Hello world!", { payload });
return {
message: "Hello world!",
};
},
});
```
<Snippet file="quickstart-example-job.mdx" />
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
![Your first Job](/images/first-job.png)
## Triggering the Job
</Step>
There are two way to trigger this Job.
<Snippet file="quickstart-running-your-job.mdx" />
1. Use the "Test" functionality in the dashboard.
2. Use the Trigger.dev API (either via our SDK or a web request)
</Steps>
### "Testing" from the dashboard
Click into the Job and then open the "Test" tab. You should see this page:
![Test Job](/images/test-job.png)
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!
## 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>
<Snippet file="quickstart-whats-next.mdx" />
+80 -1
View File
@@ -4,4 +4,83 @@ sidebarTitle: "Remix"
description: "Start creating Jobs in 5 minutes in your Remix project."
---
<Snippet file="manual-setup-remix.mdx" />
This quick start guide will get you up and running with Trigger.dev.
<Accordion title="Need to create a new Remix project to add Trigger.dev to?">
No problem, create a blank project by running the `create-remix` command in your terminal then continue with this quickstart guide as normal:
```bash
npx create-remix@latest
```
Trigger.dev works with Remix v1 and v2.
</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 Remix server together with the CLI">
You can modify your `package.json` to run both the Remix 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 remix dev command would go here
"dev:remix": "remix 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 `app/jobs/example.server.(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:
![Your first Job](/images/first-job.png)
</Step>
<Snippet file="quickstart-running-your-job.mdx" />
</Steps>
<Snippet file="quickstart-whats-next.mdx" />
+2 -2
View File
@@ -147,11 +147,11 @@ npx @trigger.dev/cli dev
Head back to your Supabase Dashboard -> Auth, and create a new user (keep "Auto Confirm User?" checked)
![create user](images/supabase-create-new-user.png)
![create user](/images/supabase-create-new-user.png)
Then navigate over to your Trigger.dev project dashboard and you should see the job running.
![job running](images/supabase-job-running.png)
![job running](/images/supabase-job-running.png)
## What's next?
+1 -1
View File
@@ -24,6 +24,6 @@ To run them, simply follow the instructions in the README files linked below.
| [OpenAI text summarizer](https://github.com/triggerdotdev/examples/tree/main/openai-text-summarizer) | An app which uses OpenAI to summarize an article and then post the result to Slack. | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) [Slack](https://trigger.dev/docs/integrations/apis/slack) | Trigger.dev | ✅ |
| [Supabase onboarding emails](https://github.com/triggerdotdev/examples/tree/main/supabase-onboarding-emails) | When a user signs up and confirms their email address, they will receive 3 "onboarding" emails over 2 days using Resend.com and Trigger.dev | [Supabase](https://trigger.dev/docs/integrations/apis/supabase) [Resend](https://trigger.dev/docs/integrations/apis/resend) | Trigger.dev | ✅ |
| [Generate presentation titles using OpenAI](https://github.com/triggerdotdev/examples/tree/main/express-vanilla) | Generate presentation titles using OpenAI background jobs with Node.js, Express and Trigger.dev | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) | [lirantal](https://github.com/lirantal) | ✅ |
| [Send a basic email with Resend](https://github.com/triggerdotdev/examples/tree/main/resend) | Send a basic email from a form with Resend | [Resend](https://trigger.dev/docs/integrations/apis/resend) | Trigger.dev | ✅ |
| [Send a basic email with Resend](https://github.com/triggerdotdev/examples/tree/main/resend-email-form) | Send a basic email from a form with Resend | [Resend](https://trigger.dev/docs/integrations/apis/resend) | Trigger.dev | ✅ |
| AI landing page copy generator | Copies your site and generates new copy using OpenAI | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) | Trigger.dev | 🛠️ |
| AI changelog generator | Generates a changelog from your GitHub commits using OpenAI | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) [GitHub](https://trigger.dev/docs/integrations/apis/github) | Trigger.dev | 🛠️ |
Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+465
View File
@@ -0,0 +1,465 @@
---
title: Linear
description: "Streamline your project and issue tracking"
---
<Snippet file="integration-getting-started.mdx" />
## Installation
To get started with the Linear integration on Trigger.dev, you need to install the `@trigger.dev/linear` package.
You can do this using npm, pnpm, or yarn:
<CodeGroup>
```bash npm
npm install @trigger.dev/linear@latest
```
```bash pnpm
pnpm add @trigger.dev/linear@latest
```
```bash yarn
yarn add @trigger.dev/linear@latest
```
</CodeGroup>
## Authentication
To use the Linear API with Trigger.dev, you can either use OAuth or a Personal API Key.
### OAuth
```ts
import { Linear } from "@trigger.dev/linear";
//this will use OAuth
const linear = new Linear({
id: "linear",
});
```
### Personal API Key
You can create a Personal API Key in your [Linear API Settings](https://linear.app/settings/api).
```ts
import { Linear } from "@trigger.dev/linear";
//this will use the passed in API key (defined in your environment variables)
const linear = new Linear({
id: "linear",
apiKey: process.env["LINEAR_API_KEY"],
});
```
## Usage
Include the Linear integration in your Trigger.dev job.
```ts
client.defineJob({
id: "linear-new-issue-autoresponder",
name: "Linear - New Issue Autoresponder",
version: "0.1.0",
integrations: {
//use the linear integration
linear,
},
//trigger on issue created events
trigger: linear.onIssueCreated(),
run: async (payload, io, ctx) => {
//get new issue ID from the event payload
const newIssueId = payload.data.id;
//comment
await io.linear.createComment("create-comment", {
issueId: newIssueId,
body: "Thank's for opening this issue!",
});
//react
await io.linear.createReaction("create-reaction", {
issueId: newIssueId,
emoji: "+1",
});
//store and display in the job run
return { payload, ctx };
},
});
```
### Serialization helper
Use the `serializeLinearOutput` helper instead of returning raw Linear SDK responses:
```ts
import { Linear, serializeLinearOutput } from "@trigger.dev/linear";
...
client.defineJob({
id: "linear-sdk",
name: "Linear SDK",
version: "0.1.0",
integrations: {
linear,
},
trigger: eventTrigger({
name: "linear.sdk",
}),
run: async (payload, io, ctx) => {
//the official Linear SDK is exposed as `client`
const issues = await io.linear.runTask("first-two", async (client) => {
//these nodes contain values we can't serialize, e.g. functions
const { nodes } = await client.issues({ first: 2 });
//we remove them with this little helper
return serializeLinearOutput(nodes);
});
return issues;
},
});
```
### Pagination
You can paginate responses two different ways:
1. Iterating the same integration task with different params
2. Using the `getAll` helper exposed on the integration (**recommended!**)
_When ordering results, make sure to use the `PaginationOrderBy` enum._
```ts
import { Linear, PaginationOrderBy, serializeLinearOutput } from "@trigger.dev/linear";
...
client.defineJob({
id: "linear-pagination",
name: "Linear Pagination",
version: "0.1.0",
integrations: {
linear,
},
trigger: eventTrigger({
name: "linear.paginate",
}),
run: async (payload, io, ctx) => {
//the same params will be used for all tasks
const params = { first: 5, orderBy: PaginationOrderBy.UpdatedAt };
//1. Linear integration - no pagination helper
let edges = await io.linear.issues("get-issues", params);
let noHelper = edges.nodes;
for (let i = 0; edges.pageInfo.hasNextPage; i++) {
edges = await io.linear.issues(`get-more-issues-${i}`, {
...params,
after: edges.pageInfo.endCursor,
});
noHelper = noHelper.concat(edges.nodes);
}
//2. Linear integration - with the pagination helper
const withHelper = await io.linear.getAll(io.linear.issues, "get-all", params);
return {
issueCounts: {
withSdk: sdkIssues.length,
noHelper: noHelper.length,
withHelper: withHelper.length,
},
};
},
});
```
## Triggers
### Attachments
| Function Name | Description |
| --------------------- | ---------------------------------------------- |
| `onAttachment` | When any action is performed on an attachment. |
| `onAttachmentCreated` | When an attachment is created. |
| `onAttachmentRemoved` | When an attachment is removed. |
| `onAttachmentUpdated` | When an attachment is updated. |
### Comments
| Function Name | Description |
| ------------------ | ------------------------------------------- |
| `onComment` | When any action is performed on an comment. |
| `onCommentCreated` | When an comment is created. |
| `onCommentRemoved` | When an comment is removed. |
| `onCommentUpdated` | When an comment is updated. |
### Cycles
| Function Name | Description |
| ---------------- | ----------------------------------------- |
| `onCycle` | When any action is performed on an cycle. |
| `onCycleCreated` | When an cycle is created. |
| `onCycleRemoved` | When an cycle is removed. |
| `onCycleUpdated` | When an cycle is updated. |
### Issues
| Function Name | Description |
| ---------------- | ----------------------------------------- |
| `onIssue` | When any action is performed on an issue. |
| `onIssueCreated` | When an issue is created. |
| `onIssueRemoved` | When an issue is removed. |
| `onIssueUpdated` | When an issue is updated. |
### Issue Labels
| Function Name | Description |
| --------------------- | ----------------------------------------------- |
| `onIssueLabel` | When any action is performed on an issue label. |
| `onIssueLabelCreated` | When an issue label is created. |
| `onIssueLabelRemoved` | When an issue label is removed. |
| `onIssueLabelUpdated` | When an issue label is updated. |
### Issue SLAs
| Function Name | Description |
| -------------------- | --------------------------------------------- |
| `onIssueSLA` | When any action is performed on an issue SLA. |
| `onIssueSLASet` | When an issue SLA is set. |
| `onIssueSLABreached` | When an issue SLA is breached. |
| `onIssueSLAHighRisk` | When an issue SLA is high risk. |
### Projects
| Function Name | Description |
| ------------------ | ------------------------------------------- |
| `onProject` | When any action is performed on an project. |
| `onProjectCreated` | When an project is created. |
| `onProjectRemoved` | When an project is removed. |
| `onProjectUpdated` | When an project is updated. |
### Project Updates
| Function Name | Description |
| ------------------------ | -------------------------------------------------- |
| `onProjectUpdate` | When any action is performed on an project update. |
| `onProjectUpdateCreated` | When an project update is created. |
| `onProjectUpdateRemoved` | When an project update is removed. |
| `onProjectUpdateUpdated` | When an project update is updated. |
### Reactions
| Function Name | Description |
| ------------------- | -------------------------------------------- |
| `onReaction` | When any action is performed on an reaction. |
| `onReactionCreated` | When an reaction is created. |
| `onReactionRemoved` | When an reaction is removed. |
| `onReactionUpdated` | When an reaction is updated. |
## Tasks
### Attachments
| Function Name | Description |
| ------------------ | -------------------------- |
| `attachment` | Gets an attachment. |
| `attachments` | Gets multiple attachments. |
| `createAttachment` | Creates an attachment. |
| `deleteAttachment` | Deletes an attachment. |
| `updateAttachment` | Updates an attachment. |
### Attachment Links
| Function Name | Description |
| ------------------------- | ------------------------------------------ |
| `attachmentLinkFront` | Links a Front conversation to an issue. |
| `attachmentLinkIntercom` | Links a Intercom conversation to an issue. |
| `attachmentLinkJiraIssue` | Links a Jira issue to an issue. |
| `attachmentLinkSlack` | Links a Slack message to an issue. |
| `attachmentLinkURL` | Links any URL to an issue. |
| `attachmentLinkZendesk` | Links a Zendesk ticket to an issue. |
### Comments
| Function Name | Description |
| --------------- | ----------------------- |
| `comment` | Gets a comment. |
| `comments` | Gets multiple comments. |
| `createComment` | Creates a comment. |
| `deleteComment` | Deletes a comment. |
| `updateComment` | Updates a comment. |
### Cycles
| Function Name | Description |
| -------------- | ----------------- |
| `archiveCycle` | Archives a cycle. |
| `createCycle` | Creates a cycle. |
| `updateCycle` | Updates a cycle. |
### Documents
| Function Name | Description |
| ----------------- | ------------------------ |
| `document` | Gets a document. |
| `documents` | Gets multiple documents. |
| `createDocument` | Creates a document. |
| `searchDocuments` | Searches documents. |
### Favorites
| Function Name | Description |
| ---------------- | ------------------------ |
| `favorite` | Gets a favorite. |
| `favorites` | Gets multiple favorites. |
| `createFavorite` | Creates a favorite. |
### Issues
| Function Name | Description |
| -------------- | --------------------- |
| `issue` | Gets an issue. |
| `issues` | Gets multiple issues. |
| `archiveIssue` | Archives an issue. |
| `createIssue` | Creates an issue. |
| `deleteIssue` | Deletes an issue. |
| `searchIssues` | Searches issues. |
| `updateIssue` | Updates an issue. |
### Issue Labels
| Function Name | Description |
| ------------------ | --------------------------- |
| `issueLabel` | Gets an issue label. |
| `issueLabels` | Gets multiple issue labels. |
| `createIssueLabel` | Creates an issue label. |
| `deleteIssueLabel` | Deletes an issue label. |
| `updateIssueLabel` | Updates an issue label. |
### Issue Relations
| Function Name | Description |
| --------------------- | ------------------------------ |
| `issueRelation` | Gets an issue relation. |
| `issueRelations` | Gets multiple issue relations. |
| `createIssueRelation` | Creates an issue relation. |
### Notifications
| Function Name | Description |
| -------------------------------- | ------------------------------------ |
| `notification` | Gets a notification. |
| `notifications` | Gets multiple notifications. |
| `archiveNotification` | Archives a notification. |
| `createNotificationSubscription` | Creates a notification subscription. |
### Organizations
| Function Name | Description |
| ---------------------------------- | ------------------------------- |
| `organization` | Gets the viewer's organization. |
| `createOrganizationFromOnboarding` | Creates an organization. |
| `createOrganizationInvite` | Creates an organization invite. |
### Projects
| Function Name | Description |
| ---------------- | ----------------------- |
| `project` | Gets a project. |
| `projects` | Gets multiple projects. |
| `archiveProject` | Archives a project. |
| `createProject` | Creates a project. |
| `deleteProject` | Deletes a project. |
| `searchProjects` | Searches projects. |
| `updateProject` | Updates a project. |
### Project Links
| Function Name | Description |
| ------------------- | ---------------------------- |
| `projectLink` | Gets a project link. |
| `projectLinks` | Gets multiple project links. |
| `createProjectLink` | Creates a project link. |
### Project Updates
| Function Name | Description |
| --------------------- | ------------------------------ |
| `projectUpdate` | Gets a project update. |
| `projectUpdates` | Gets multiple project updates. |
| `createProjectUpdate` | Creates a project update. |
| `deleteProjectUpdate` | Deletes a project update. |
| `updateProjectUpdate` | Updates a project update. |
### Reactions
| Function Name | Description |
| ---------------- | ------------------- |
| `createReaction` | Creates a reaction. |
| `deleteReaction` | Deletes a reaction. |
### Roadmaps
| Function Name | Description |
| ---------------- | ------------------- |
| `archiveRoadmap` | Archives a roadmap. |
| `createRoadmap` | Creates a roadmap. |
### Teams
| Function Name | Description |
| ------------- | -------------------- |
| `team` | Gets a team. |
| `teams` | Gets multiple teams. |
| `createTeam` | Creates a team. |
### Team Memberships
| Function Name | Description |
| ---------------------- | ------------------------------- |
| `teamMembership` | Gets a team membership. |
| `teamMemberships` | Gets multiple team memberships. |
| `createTeamMembership` | Creates a team membership. |
### Templates
| Function Name | Description |
| ------------- | ------------------------ |
| `template` | Gets a template. |
| `templates` | Gets multiple templates. |
### Users
| Function Name | Description |
| ------------- | -------------------- |
| `user` | Gets a user. |
| `users` | Gets multiple users. |
| `updateUser` | Updates a user. |
### Webhooks
| Function Name | Description |
| --------------- | ----------------------- |
| `webhook` | Gets a webhook. |
| `webhooks` | Gets multiple webhooks. |
| `createWebhook` | Creates a webhook. |
| `deleteWebhook` | Deletes a webhook. |
| `updateWebhook` | Updates a webhook. |
### Workflow States
| Function Name | Description |
| ---------------------- | ------------------------------ |
| `workflowState` | Gets a workflow state. |
| `workflowStates` | Gets multiple workflow states. |
| `archiveWorkflowState` | Archives a workflow state. |
| `createWorkflowState` | Creates a workflow state. |
### Misc
| Function Name | Description |
| ------------------------ | -------------------------------------- |
| `createProjectMilestone` | Creates a project milestone. |
| `issuePriorityValues` | Gets issue priority values and labels. |
| `viewer` | Gets the currently authenticated user. |
+1 -1
View File
@@ -8,7 +8,7 @@ description: "Publishing an official integration package to the public registry.
Ensure that you have done the following:
- [ ] Create the integrations in the `integrations` folder.
- [ ] Created a file in the `examples/job-catalog` folder with some example jobs.
- [ ] Created a file in the `references/job-catalog` folder with some example jobs.
- [ ] Have exported types.
- [ ] Avoid using `any` types and `@ts-ignore` comments. If there are any, please explain them in the PR.
+6 -6
View File
@@ -3,17 +3,17 @@ title: Testing
description: "To test your integration you should create some example Jobs where you test all the functionality."
---
# examples/job-catalog
# references/job-catalog
When building an official integration (in the Trigger.dev monorepo) you should use the job-catalog to test your integration. This collection of examples is useful for creating docs as well as testing.
When building an official integration (in the Trigger.dev monorepo) you should use the job-catalog to test your integration. This collection of reference jobs is useful for creating docs as well as testing.
## 1. Getting setup
You can follow [the README](https://github.com/triggerdotdev/trigger.dev/tree/main/examples/job-catalog) to get setup. It explains how to add a new Job as well.
You can follow [the README](https://github.com/triggerdotdev/trigger.dev/tree/main/references/job-catalog) to get setup. It explains how to add a new Job as well.
## 2. Adding your package
You'll need to add your integration to [the package.json file](https://github.com/triggerdotdev/trigger.dev/blob/main/examples/job-catalog/package.json):
You'll need to add your integration to [the package.json file](https://github.com/triggerdotdev/trigger.dev/blob/main/references/job-catalog/package.json):
```json
{
@@ -25,7 +25,7 @@ You'll need to add your integration to [the package.json file](https://github.co
}
```
And to [the tsconfig.json file](https://github.com/triggerdotdev/trigger.dev/blob/main/examples/job-catalog/tsconfig.json):
And to [the tsconfig.json file](https://github.com/triggerdotdev/trigger.dev/blob/main/references/job-catalog/tsconfig.json):
```json
{
@@ -47,4 +47,4 @@ And to [the tsconfig.json file](https://github.com/triggerdotdev/trigger.dev/blo
## 3. Adding your Jobs
Create a new file in `src` that uses all the features of your integration. [The Typeform one](https://github.com/triggerdotdev/trigger.dev/blob/main/examples/job-catalog/src/typeform.ts) is a useful reference that has Tasks and Triggers.
Create a new file in `src` that uses all the features of your integration. [The Typeform one](https://github.com/triggerdotdev/trigger.dev/blob/main/references/job-catalog/src/typeform.ts) is a useful reference that has Tasks and Triggers.
+1
View File
@@ -33,6 +33,7 @@ Navigate the menu or select Integrations from the table below.
| API | Description | Webhooks | Tasks |
| --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- |
| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ |
| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ |
| [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ |
| [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ |
| [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ |
+28 -8
View File
@@ -151,7 +151,6 @@
"documentation/guides/manual/fastify"
]
},
"documentation/guides/running-jobs",
"documentation/guides/jobs/managing",
{
@@ -168,10 +167,18 @@
"pages": [
"documentation/guides/using-integrations",
"documentation/guides/using-integrations-apikeys",
"documentation/guides/using-integrations-oauth"
"documentation/guides/using-integrations-oauth",
"documentation/guides/using-integrations-byo-auth"
]
},
{
"group": "React hooks",
"pages": [
"documentation/guides/react-hooks",
"documentation/guides/react-hooks-automatic",
"documentation/guides/react-hooks-statuses"
]
},
"documentation/guides/react-hooks",
{
"group": "Deployment",
"pages": [
@@ -237,6 +244,7 @@
"integrations/apis/github-tasks"
]
},
"integrations/apis/linear",
"integrations/apis/openai",
"integrations/apis/plain",
"integrations/apis/resend",
@@ -269,7 +277,11 @@
"sdk/triggerclient/instancemethods/sendevent",
"sdk/triggerclient/instancemethods/getevent",
"sdk/triggerclient/instancemethods/getruns",
"sdk/triggerclient/instancemethods/getrun"
"sdk/triggerclient/instancemethods/getrun",
"sdk/triggerclient/instancemethods/define-job",
"sdk/triggerclient/instancemethods/define-dynamic-trigger",
"sdk/triggerclient/instancemethods/define-dynamic-schedule",
"sdk/triggerclient/instancemethods/define-auth-resolver"
]
}
]
@@ -303,7 +315,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -314,7 +329,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -335,7 +353,9 @@
},
{
"group": "Overview",
"pages": ["examples/introduction"]
"pages": [
"examples/introduction"
]
}
],
"footerSocials": {
@@ -348,4 +368,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+8 -5
View File
@@ -34,7 +34,7 @@ Use this method to unregister a schedule from the DynamicSchedule, using the id
```typescript
//1. create a DynamicSchedule
const dynamicSchedule = new DynamicSchedule(client, {
const dynamicSchedule = client.defineDynamicSchedule({
id: "dynamicinterval",
});
@@ -76,14 +76,17 @@ client.defineJob({
}),
run: async (payload, io, ctx) => {
//6. Register the DynamicSchedule
await io.registerInterval("📆", dynamicSchedule, payload.userId, {
seconds: payload.seconds,
await dynamicSchedule.register(payload.userId, {
type: "interval",
options: {
seconds: payload.seconds,
},
});
await io.wait("wait", 60);
//7. Unregister the DynamicSchedule if you want
await io.unregisterInterval("❌📆", dynamicSchedule, payload.id);
//7. Unregister the DynamicSchedule at some later date
await dynamicSchedule.unregister(payload.userId);
},
});
```

Some files were not shown because too many files have changed in this diff Show More