diff --git a/.changeset/config.json b/.changeset/config.json index 64bfe8e522..5e8716336b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -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": [ [ diff --git a/.dockerignore b/.dockerignore index 53f29c1c74..2b8f2382bc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example index 295b41745c..48170e8f36 100644 --- a/.env.example +++ b/.env.example @@ -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= \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..c5d01d1919 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3084379011 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Ask a Question + url: https://trigger.dev/discord + about: Ask questions and discuss with other community members diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..bcb3dee02a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..279bab91a7 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,12 @@ +"πŸ“Œ area: cli": + - any: ["cli/**/*"] + +"πŸ“Œ area: t3-app": + - any: ["cli/template/**/*"] + +"πŸ“š documentation": + - any: ["www/**/*"] + - any: ["**/*.md"] + +"πŸ“Œ area: ci": + - any: [".github/**/*"] \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..eec6c72820 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +Closes # + +## βœ… 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]_ + +πŸ’― diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a1dd2a0a6..74282ef693 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -129,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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 21288b65a1..37375ca33d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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 } ] } diff --git a/CHANGESETS.md b/CHANGESETS.md index b16222c103..9565e908ce 100644 --- a/CHANGESETS.md +++ b/CHANGESETS.md @@ -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` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3732dd9ad8..471123fd38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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//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//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 `` 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. diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index 2f501ea9a1..245eeff8ba 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -106,7 +106,7 @@ export function TriggerDevStep() { - If you’re not running on port 3000 you can specify the port by adding{" "} + If you’re not running on the default you can specify the port by adding{" "} --port 3001 to the end. diff --git a/apps/webapp/app/components/code/InstallPackages.tsx b/apps/webapp/app/components/code/InstallPackages.tsx new file mode 100644 index 0000000000..791d101daa --- /dev/null +++ b/apps/webapp/app/components/code/InstallPackages.tsx @@ -0,0 +1,44 @@ +import { + ClientTabs, + ClientTabsList, + ClientTabsTrigger, + ClientTabsContent, +} from "../primitives/ClientTabs"; +import { ClipboardField } from "../primitives/ClipboardField"; + +type InstallPackagesProps = { + packages: string[]; +}; + +export function InstallPackages({ packages }: InstallPackagesProps) { + return ( + + + npm + pnpm + yarn + + + + + + + + + + + + ); +} diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index 4b635904a1..7d5ef0c237 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -1,11 +1,13 @@ import { json as jsonLang } from "@codemirror/lang-json"; import type { ViewUpdate } from "@codemirror/view"; +import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid"; import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror"; import { useCodeMirror } from "@uiw/react-codemirror"; -import { useRef, useEffect } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { cn } from "~/utils/cn"; +import { Button } from "../primitives/Buttons"; import { getEditorSetup } from "./codeMirrorSetup"; import { darkTheme } from "./codeMirrorTheme"; -import { cn } from "~/utils/cn"; export interface JSONEditorProps extends Omit { defaultValue?: string; @@ -14,6 +16,8 @@ export interface JSONEditorProps extends Omit { onChange?: (value: string) => void; onUpdate?: (update: ViewUpdate) => void; onBlur?: (code: string) => void; + showCopyButton?: boolean; + showClearButton?: boolean; } const languages = { @@ -38,6 +42,8 @@ export function JSONEditor(opts: JSONEditorProps) { onBlur, basicSetup, autoFocus, + showCopyButton = true, + showClearButton = true, } = { ...defaultProps, ...opts, @@ -65,7 +71,8 @@ export function JSONEditor(opts: JSONEditorProps) { onChange, onUpdate, }; - const { setContainer, state } = useCodeMirror(settings); + const { setContainer, view } = useCodeMirror(settings); + const [copied, setCopied] = useState(false); useEffect(() => { if (editor.current) { @@ -75,24 +82,71 @@ export function JSONEditor(opts: JSONEditorProps) { //if the defaultValue changes update the editor useEffect(() => { - if (state !== undefined) { - state.update({ - changes: { from: 0, to: state.doc.length, insert: defaultValue }, + if (view !== undefined) { + if (view.state.doc.toString() === defaultValue) return; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: defaultValue }, }); } - }, [defaultValue, state]); + }, [defaultValue, view]); + + const clear = useCallback(() => { + if (view === undefined) return; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: undefined }, + }); + onChange?.(""); + }, [view]); + + const copy = useCallback(() => { + if (view === undefined) return; + navigator.clipboard.writeText(view.state.doc.toString()); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1500); + }, [view]); return ( -
{ - if (!onBlur) return; - onBlur(editor.current?.textContent ?? ""); - }} - /> +
+
{ + if (!onBlur) return; + onBlur(editor.current?.textContent ?? ""); + }} + /> +
+ {showClearButton && ( + + )} + {showCopyButton && ( + + )} +
+
); } diff --git a/apps/webapp/app/components/code/codeMirrorSetup.ts b/apps/webapp/app/components/code/codeMirrorSetup.ts index 9e7ff711df..89988c3d9b 100644 --- a/apps/webapp/app/components/code/codeMirrorSetup.ts +++ b/apps/webapp/app/components/code/codeMirrorSetup.ts @@ -1,34 +1,19 @@ +import { closeBrackets } from "@codemirror/autocomplete"; +import { indentWithTab } from "@codemirror/commands"; +import { jsonParseLinter } from "@codemirror/lang-json"; +import { bracketMatching } from "@codemirror/language"; +import { lintGutter, lintKeymap, linter } from "@codemirror/lint"; +import { highlightSelectionMatches } from "@codemirror/search"; +import { Prec, type Extension } from "@codemirror/state"; import { - highlightSpecialChars, drawSelection, - highlightActiveLine, dropCursor, - lineNumbers, + highlightActiveLine, highlightActiveLineGutter, + highlightSpecialChars, keymap, + lineNumbers, } from "@codemirror/view"; -import type { Extension } from "@codemirror/state"; -import { highlightSelectionMatches } from "@codemirror/search"; -import { json as jsonLang } from "@codemirror/lang-json"; -import { closeBrackets } from "@codemirror/autocomplete"; -import { bracketMatching } from "@codemirror/language"; -import { indentWithTab } from "@codemirror/commands"; - -export function getPreviewSetup(): Array { - return [ - jsonLang(), - highlightSpecialChars(), - drawSelection(), - dropCursor(), - bracketMatching(), - highlightSelectionMatches(), - lineNumbers(), - ]; -} - -export function getViewerSetup(): Array { - return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()]; -} export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array { const options = [ @@ -36,7 +21,20 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A dropCursor(), bracketMatching(), closeBrackets(), - keymap.of([indentWithTab]), + lintGutter(), + linter(jsonParseLinter()), + Prec.highest( + keymap.of([ + { + key: "Mod-Enter", + run: () => { + return true; + }, + preventDefault: false, + }, + ]) + ), + keymap.of([indentWithTab, ...lintKeymap]), ]; if (showLineNumbers) { diff --git a/apps/webapp/app/components/code/codeMirrorTheme.ts b/apps/webapp/app/components/code/codeMirrorTheme.ts index 0492b80b93..babcccf09e 100644 --- a/apps/webapp/app/components/code/codeMirrorTheme.ts +++ b/apps/webapp/app/components/code/codeMirrorTheme.ts @@ -17,10 +17,15 @@ export function darkTheme(): Extension { violet = "#c678dd", darkBackground = "#21252b", highlightBackground = "rgba(71,85,105,0.2)", - background = "#0f172a", + background = "rgba(11, 16, 24 ,100)", tooltipBackground = "#353a42", selection = "rgb(71 85 105)", - cursor = "#528bff"; + cursor = "#528bff", + scrollbarTrack = "#0E1521", + scrollbarTrackActive = "#131B2B", + scrollbarThumb = "#293649", + scrollbarThumbActive = "#3C4B62", + scrollbarBg = "#0E1521"; const jsonHeroEditorTheme = EditorView.theme( { @@ -94,6 +99,45 @@ export function darkTheme(): Extension { color: ivory, }, }, + ".cm-scroller": { + scrollbarWidth: "thin", + scrollbarColor: `${scrollbarThumb} ${scrollbarTrack}`, + }, + ".cm-scroller::-webkit-scrollbar": { + display: "block", + width: "8px", + height: "8px", + }, + ".cm-scroller::-webkit-scrollbar-track": { + backgroundColor: scrollbarTrack, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-track:hover": { + backgroundColor: scrollbarTrackActive, + }, + ".cm-scroller::-webkit-scrollbar-track:active": { + backgroundColor: scrollbarTrackActive, + }, + ".cm-scroller::-webkit-scrollbar-thumb": { + backgroundColor: scrollbarThumb, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-thumb:hover": { + backgroundColor: scrollbarThumbActive, + }, + ".cm-scroller::-webkit-scrollbar-thumb:active": { + backgroundColor: scrollbarThumbActive, + }, + ".cm-scroller::-webkit-scrollbar-corner": { + backgroundColor: scrollbarBg, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-corner:hover": { + backgroundColor: scrollbarBg, + }, + ".cm-scroller::-webkit-scrollbar-corner:active": { + backgroundColor: scrollbarBg, + }, }, { dark: true } ); @@ -155,157 +199,3 @@ export function darkTheme(): Extension { return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)]; } - -export function lightTheme(): Extension[] { - const stringColor = "text-[#53a053]", - numberColor = "text-[#447bef]", - variableColor = "text-[#a42ea2]", - booleanColor = "text-[#e2574e]", - coral = "text-[#e06c75]", - invalid = "text-[#ffffff]", - ivory = "text-[#abb2bf]", - stone = "text-[#7d8799]", - malibu = "text-[#61afef]", - whiskey = "text-[#d19a66]", - violet = "text-[#c678dd]", - darkBackground = "text-[#21252b]", - highlightBackground = "text-[#D0D0D0]", - background = "text-[#ffffff]", - tooltipBackground = "text-[#353a42]", - selection = "text-[#D0D0D0]", - cursor = "text-[#528bff]"; - - const jsonHeroEditorTheme = EditorView.theme( - { - "&": { - color: ivory, - backgroundColor: background, - }, - - ".cm-content": { - caretColor: cursor, - fontFamily: "monospace", - fontSize: "14px", - }, - - ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { - backgroundColor: selection, - }, - - ".cm-panels": { backgroundColor: darkBackground, color: ivory }, - ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, - ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, - - ".cm-searchMatch": { - backgroundColor: "#72a1ff59", - outline: "1px solid #457dff", - }, - ".cm-searchMatch.cm-searchMatch-selected": { - backgroundColor: "#6199ff2f", - }, - - ".cm-activeLine": { backgroundColor: highlightBackground }, - ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, - - "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { - backgroundColor: "#bad0f847", - outline: "1px solid #515a6b", - }, - - ".cm-gutters": { - backgroundColor: background, - color: stone, - border: "none", - }, - - ".cm-activeLineGutter": { - backgroundColor: highlightBackground, - }, - - ".cm-foldPlaceholder": { - backgroundColor: "transparent", - border: "none", - color: "#ddd", - }, - - ".cm-tooltip": { - border: "none", - backgroundColor: tooltipBackground, - }, - ".cm-tooltip .cm-tooltip-arrow:before": { - borderTopColor: "transparent", - borderBottomColor: "transparent", - }, - ".cm-tooltip .cm-tooltip-arrow:after": { - borderTopColor: tooltipBackground, - borderBottomColor: tooltipBackground, - }, - ".cm-tooltip-autocomplete": { - "& > ul > li[aria-selected]": { - backgroundColor: highlightBackground, - color: ivory, - }, - }, - }, - { dark: false } - ); - - /// The highlighting style for code in the JSON Hero theme. - const jsonHeroHighlightStyle = tagHighlighter([ - { tag: tags.keyword, class: violet }, - { - tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], - class: variableColor, - }, - { - tag: [tags.function(tags.variableName), tags.labelName], - class: malibu, - }, - { - tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], - class: whiskey, - }, - { tag: [tags.definition(tags.name), tags.separator], class: ivory }, - { - tag: [ - tags.typeName, - tags.className, - tags.number, - tags.changed, - tags.annotation, - tags.modifier, - tags.self, - tags.namespace, - ], - class: numberColor, - }, - { - tag: [ - tags.operator, - tags.operatorKeyword, - tags.url, - tags.escape, - tags.regexp, - tags.link, - tags.special(tags.string), - ], - class: stringColor, - }, - { tag: [tags.meta, tags.comment], class: stone }, - - { tag: tags.link, class: stone }, - { tag: tags.heading, class: coral }, - { - tag: [tags.atom, tags.bool, tags.special(tags.variableName)], - class: booleanColor, - }, - { - tag: [tags.processingInstruction, tags.string, tags.inserted], - class: stringColor, - }, - { tag: tags.invalid, class: invalid }, - ]); - - return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)]; -} diff --git a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx index 1e8fd1adb0..0dd9849b81 100644 --- a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx +++ b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx @@ -51,28 +51,28 @@ export function FrameworkSelector() { - + - - - + + + - + - +
diff --git a/apps/webapp/app/components/helpContent/HelpContentText.tsx b/apps/webapp/app/components/helpContent/HelpContentText.tsx index 9b7adb3e48..ce057dae48 100644 --- a/apps/webapp/app/components/helpContent/HelpContentText.tsx +++ b/apps/webapp/app/components/helpContent/HelpContentText.tsx @@ -79,37 +79,6 @@ export function HowToRunYourJob() { ); } -export function HowToRunATest() { - return ( - <> - - - Select the environment you’d like the test to run against. - - - - - - Write your own payload specific to your Job. Some Triggers also provide example payloads - that you can select from. This will populate the code editor below. - - - - - - When you’re happy with the payload, click Run test. - - - Learn more about running tests. - - - ); -} - export function HowToConnectAnIntegration() { return ( <> @@ -272,6 +241,21 @@ export function HowToUseApiKeysAndEndpoints() { you should use the Test feature to trigger any scheduled Jobs. + + Staging + + + } + /> + + + The STAGING environment is where your Jobs will run in a staging + environment, meant to mirror your production environment. + + @@ -108,14 +109,19 @@ export function SelectOAuthMethod({ ) ) : ( <> - User OAuth coming soon + BYO Auth - End-user OAuth is going to be released soon. If you are interested in being an early - beta tester then please{" "} - - message us - - . + We support external authentication providers through Auth Resolvers. Read the docs to + learn more:{" "} + + Bring your own Auth + ))} diff --git a/apps/webapp/app/components/jobs/JobsTable.tsx b/apps/webapp/app/components/jobs/JobsTable.tsx index 7b15074d25..8f2e4fb84c 100644 --- a/apps/webapp/app/components/jobs/JobsTable.tsx +++ b/apps/webapp/app/components/jobs/JobsTable.tsx @@ -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 ""; diff --git a/apps/webapp/app/components/navigation/ProjectSideMenu.tsx b/apps/webapp/app/components/navigation/ProjectSideMenu.tsx index 34d1751db8..91a6cdf6dc 100644 --- a/apps/webapp/app/components/navigation/ProjectSideMenu.tsx +++ b/apps/webapp/app/components/navigation/ProjectSideMenu.tsx @@ -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" />
-
+
+ ; name: string; @@ -153,6 +162,7 @@ function SideMenuItem({ isCollapsed: boolean; hasWarning?: boolean; forceActive?: boolean; + target?: string; }) { return ( { 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" ); diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 1dcc157454..e469c2561b 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -144,7 +144,7 @@ export function ButtonContent(props: ButtonContentPropsType) { const textColorClassName = variation.textColor; return ( -
+
& Omit, "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 ( - + {({ isActive, isPending }) => ( )} diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index fcc9478cb0..a9d618b862 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -63,16 +63,17 @@ export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => { }; function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string { - const milliseconds = `00${date.getMilliseconds()}`.slice(-3); - const formattedDateTime = new Intl.DateTimeFormat(locales, { + year: "numeric", month: "short", - day: "2-digit", + day: "numeric", hour: "numeric", - minute: "2-digit", - second: "2-digit", + minute: "numeric", + second: "numeric", timeZone, + // @ts-ignore this works in 92.5% of browsers https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_options_parameter_options_fractionalseconddigits_parameter + fractionalSecondDigits: 3, }).format(date); - return `${formatDateTime}.${milliseconds}`; + return formattedDateTime; } diff --git a/apps/webapp/app/components/primitives/DetailCell.tsx b/apps/webapp/app/components/primitives/DetailCell.tsx new file mode 100644 index 0000000000..6047cc150f --- /dev/null +++ b/apps/webapp/app/components/primitives/DetailCell.tsx @@ -0,0 +1,95 @@ +import { cn } from "~/utils/cn"; +import { Icon, IconInBox, RenderIcon } from "./Icon"; +import { Paragraph } from "./Paragraph"; + +const variations = { + small: { + label: { + variant: "small" as const, + className: "m-0 leading-[1.1rem]", + }, + description: { + variant: "extra-small" as const, + className: "m-0", + }, + }, + base: { + label: { + variant: "base" as const, + className: "m-0 leading-[1.1rem] ", + }, + description: { + variant: "small" as const, + className: "m-0", + }, + }, +}; + +type DetailCellProps = { + leadingIcon?: RenderIcon; + leadingIconClassName?: string; + trailingIcon?: RenderIcon; + trailingIconClassName?: string; + label: string | React.ReactNode; + description?: string | React.ReactNode; + className?: string; + variant?: keyof typeof variations; +}; + +export function DetailCell({ + leadingIcon, + leadingIconClassName, + trailingIcon, + trailingIconClassName, + label, + description, + className, + variant = "small", +}: DetailCellProps) { + const variation = variations[variant]; + + return ( +
+ +
+ + {label} + + {description && ( + + {description} + + )} +
+
+ +
+
+ ); +} diff --git a/apps/webapp/app/components/primitives/Icon.tsx b/apps/webapp/app/components/primitives/Icon.tsx new file mode 100644 index 0000000000..470d4cc809 --- /dev/null +++ b/apps/webapp/app/components/primitives/Icon.tsx @@ -0,0 +1,37 @@ +import { IconNamesOrString, NamedIcon } from "./NamedIcon"; +import { cn } from "~/utils/cn"; + +export type RenderIcon = IconNamesOrString | React.ComponentType; + +type IconProps = { + icon?: RenderIcon; + className?: string; +}; + +/** Use this icon to either render a passed in React component, or a NamedIcon/CompanyIcon */ +export function Icon(props: IconProps) { + if (typeof props.icon === "string") { + return } />; + } + + const Icon = props.icon; + + if (!Icon) { + return <>; + } + + return ; +} + +export function IconInBox({ boxClassName, ...props }: IconProps & { boxClassName?: string }) { + return ( +
+ +
+ ); +} diff --git a/apps/webapp/app/components/primitives/NamedIcon.tsx b/apps/webapp/app/components/primitives/NamedIcon.tsx index 6cbe7d8c46..e06656e90e 100644 --- a/apps/webapp/app/components/primitives/NamedIcon.tsx +++ b/apps/webapp/app/components/primitives/NamedIcon.tsx @@ -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) => , background: (className: string) => , beaker: (className: string) => , + bell: (className: string) => , billing: (className: string) => , + browser: (className: string) => , calendar: (className: string) => ( ), @@ -111,6 +115,7 @@ const icons = { ), heart: (className: string) => , + house: (className: string) => , id: (className: string) => , inactive: (className: string) => , info: (className: string) => , @@ -126,6 +131,7 @@ const icons = { "clipboard-checked": (className: string) => ( ), + list: (className: string) => , log: (className: string) => ( ), diff --git a/apps/webapp/app/components/primitives/ShortcutKey.tsx b/apps/webapp/app/components/primitives/ShortcutKey.tsx index ff5d4f5f70..8be993c12e 100644 --- a/apps/webapp/app/components/primitives/ShortcutKey.tsx +++ b/apps/webapp/app/components/primitives/ShortcutKey.tsx @@ -23,7 +23,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) const isMac = platform === "mac"; let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; const modifiers = relevantShortcut.modifiers ?? []; - const character = relevantShortcut.key; + const character = keyString(relevantShortcut.key, isMac); return ( @@ -35,6 +35,15 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) ); } +function keyString(key: String, isMac: boolean) { + switch (key) { + case "enter": + return isMac ? "↡" : key; + default: + return key; + } +} + function modifierString(modifier: Modifier, isMac: boolean) { switch (modifier) { case "alt": @@ -42,8 +51,10 @@ function modifierString(modifier: Modifier, isMac: boolean) { case "ctrl": return isMac ? "βŒƒ" : "Ctrl+"; case "meta": - return isMac ? "⌘" : "⊞"; + return isMac ? "⌘" : "⊞+"; case "shift": return isMac ? "⇧" : "Shift+"; + case "mod": + return isMac ? "⌘" : "Ctrl+"; } } diff --git a/apps/webapp/app/components/run/RunCompletedDetail.tsx b/apps/webapp/app/components/run/RunCompletedDetail.tsx index 5d1641c9e2..e35396a727 100644 --- a/apps/webapp/app/components/run/RunCompletedDetail.tsx +++ b/apps/webapp/app/components/run/RunCompletedDetail.tsx @@ -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, diff --git a/apps/webapp/app/components/run/RunOverview.tsx b/apps/webapp/app/components/run/RunOverview.tsx index 7abceb941f..f9086d3c66 100644 --- a/apps/webapp/app/components/run/RunOverview.tsx +++ b/apps/webapp/app/components/run/RunOverview.tsx @@ -12,7 +12,7 @@ import { import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database"; import { useMemo } from "react"; import { usePathName } from "~/hooks/usePathName"; -import { Run } from "~/presenters/RunPresenter.server"; +import { ViewRun } from "~/presenters/RunPresenter.server"; import { cancelSchema } from "~/routes/resources.runs.$runId.cancel"; import { schema } from "~/routes/resources.runs.$runId.rerun"; import { formatDuration } from "~/utils"; @@ -59,7 +59,7 @@ import { TaskCard } from "./TaskCard"; import { TaskCardSkeleton } from "./TaskCardSkeleton"; type RunOverviewProps = { - run: Run; + run: ViewRun; trigger: { icon: string; title: string; @@ -167,7 +167,13 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps diff --git a/apps/webapp/app/components/run/TaskCard.tsx b/apps/webapp/app/components/run/TaskCard.tsx index c1a399d38c..1bce2f482d 100644 --- a/apps/webapp/app/components/run/TaskCard.tsx +++ b/apps/webapp/app/components/run/TaskCard.tsx @@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Fragment, useState } from "react"; import simplur from "simplur"; import { Paragraph } from "~/components/primitives/Paragraph"; -import { Task } from "~/presenters/RunPresenter.server"; +import { ViewTask } from "~/presenters/RunPresenter.server"; import { formatDuration } from "~/utils"; import { cn } from "~/utils/cn"; import { @@ -22,7 +22,7 @@ import { } from "./RunCard"; import { TaskStatusIcon } from "./TaskStatus"; -type TaskCardProps = Task & { +type TaskCardProps = ViewTask & { selectedId?: string; selectedTask: (id: string) => void; isLast: boolean; diff --git a/apps/webapp/app/components/run/TriggerDetail.tsx b/apps/webapp/app/components/run/TriggerDetail.tsx index 006f3b5769..aee57b22ea 100644 --- a/apps/webapp/app/components/run/TriggerDetail.tsx +++ b/apps/webapp/app/components/run/TriggerDetail.tsx @@ -25,7 +25,7 @@ export function TriggerDetail({ }; properties: DisplayProperty[]; }) { - const { id, name, payload, timestamp, deliveredAt } = trigger; + const { id, name, payload, context, timestamp, deliveredAt } = trigger; return ( @@ -45,6 +45,14 @@ export function TriggerDetail({ /> )} + + {trigger.externalAccount && ( + + )}
@@ -55,7 +63,9 @@ export function TriggerDetail({
)} Payload - + + Context +
diff --git a/apps/webapp/app/components/runs/RunStatuses.tsx b/apps/webapp/app/components/runs/RunStatuses.tsx index 0a2e16827d..828fe50b47 100644 --- a/apps/webapp/app/components/runs/RunStatuses.tsx +++ b/apps/webapp/app/components/runs/RunStatuses.tsx @@ -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 ; case "TIMED_OUT": return ; + case "UNRESOLVED_AUTH": + case "INVALID_PAYLOAD": + return ; case "WAITING_ON_CONNECTIONS": return ; 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"; diff --git a/apps/webapp/app/components/stories/DetailCell.stories.tsx b/apps/webapp/app/components/stories/DetailCell.stories.tsx new file mode 100644 index 0000000000..7f70e01a16 --- /dev/null +++ b/apps/webapp/app/components/stories/DetailCell.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { DetailCell } from "../primitives/DetailCell"; +import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline"; +import { DateTime, DateTimeAccurate } from "../primitives/DateTime"; + +const meta: Meta = { + title: "Primitives/DetailCells", +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: () => , +}; + +function Examples() { + return ( +
+ + + } + description="Run #42 complete" + trailingIcon="plus" + trailingIconClassName="text-slate-500 group-hover:text-bright" + /> +
+ ); +} diff --git a/apps/webapp/app/components/stories/Shortcuts.stories.tsx b/apps/webapp/app/components/stories/Shortcuts.stories.tsx index a5ed28866a..dc2f381a75 100644 --- a/apps/webapp/app/components/stories/Shortcuts.stories.tsx +++ b/apps/webapp/app/components/stories/Shortcuts.stories.tsx @@ -24,6 +24,8 @@ const shortcuts: ShortcutDefinition[] = [ { key: "f", modifiers: ["meta"] }, { key: "k", modifiers: ["meta"] }, { key: "del", modifiers: ["ctrl", "alt"] }, + { key: "enter", modifiers: ["meta"] }, + { key: "enter", modifiers: ["mod"] }, ]; function Collection() { @@ -67,6 +69,9 @@ function Set({ platform }: { platform: "mac" | "windows" }) { +
))} diff --git a/apps/webapp/app/consts.ts b/apps/webapp/app/consts.ts index c6e9c85cdd..51ab2cc444 100644 --- a/apps/webapp/app/consts.ts +++ b/apps/webapp/app/consts.ts @@ -5,3 +5,4 @@ export const DEFAULT_MAX_CONCURRENT_RUNS = 10; export const MAX_CONCURRENT_RUNS_LIMIT = 20; export const PREPROCESS_RETRY_LIMIT = 2; export const EXECUTE_JOB_RETRY_LIMIT = 10; +export const MAX_RUN_YIELDED_EXECUTIONS = 100; diff --git a/apps/webapp/app/db.server.ts b/apps/webapp/app/db.server.ts index a6a58527cd..171a5b79dc 100644 --- a/apps/webapp/app/db.server.ts +++ b/apps/webapp/app/db.server.ts @@ -31,7 +31,7 @@ export type PrismaTransactionOptions = { /** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */ isolationLevel?: Prisma.TransactionIsolationLevel; - rethrowPrismaErrors?: boolean; + swallowPrismaErrors?: boolean; }; export async function $transaction( @@ -55,11 +55,9 @@ export async function $transaction( name: error.name, }); - if (options?.rethrowPrismaErrors) { - throw error; + if (options?.swallowPrismaErrors) { + return; } - - return; } throw error; @@ -124,6 +122,10 @@ function getClient() { emit: "stdout", level: "warn", }, + // { + // emit: "stdout", + // level: "query", + // }, ], }); diff --git a/apps/webapp/app/hooks/useShortcutKeys.tsx b/apps/webapp/app/hooks/useShortcutKeys.tsx index ccbebf618e..092e163967 100644 --- a/apps/webapp/app/hooks/useShortcutKeys.tsx +++ b/apps/webapp/app/hooks/useShortcutKeys.tsx @@ -1,12 +1,12 @@ -import { useEffect, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider"; -export type Modifier = "alt" | "ctrl" | "meta" | "shift"; +export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod"; export type Shortcut = { key: string; modifiers?: Modifier[]; + enabledOnInputElements?: boolean; }; export type ShortcutDefinition = @@ -20,19 +20,31 @@ type useShortcutKeysProps = { shortcut: ShortcutDefinition; action: (event: KeyboardEvent) => void; disabled?: boolean; + enabledOnInputElements?: boolean; }; export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) { - const keys = createKeysFromShortcut(shortcut); - useHotkeys(keys, action, { enabled: !disabled }); -} - -function createKeysFromShortcut(shortcut: ShortcutDefinition) { const { platform } = useOperatingSystem(); const isMac = platform === "mac"; - let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; - const modifiers = relevantShortcut.modifiers; - const character = relevantShortcut.key; + const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; + + const keys = createKeysFromShortcut(relevantShortcut); + useHotkeys( + keys, + (event, hotkeysEvent) => { + action(event); + }, + { + enabled: !disabled, + enableOnFormTags: relevantShortcut.enabledOnInputElements, + enableOnContentEditable: relevantShortcut.enabledOnInputElements, + } + ); +} + +function createKeysFromShortcut(shortcut: Shortcut) { + const modifiers = shortcut.modifiers; + const character = shortcut.key; - return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character; + return modifiers ? modifiers.map((k) => k).join("+") + "+" + character : character; } diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index da9db9a541..2daf0791a5 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -8,7 +8,6 @@ import type { import { customAlphabet } from "nanoid"; import slug from "slug"; import { prisma, PrismaClientOrTransaction } from "~/db.server"; -import { workerQueue } from "~/services/worker.server"; import { createProject } from "./project.server"; export type { Organization }; @@ -76,6 +75,10 @@ export async function createOrganization( }, attemptCount = 0 ): Promise { + if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) { + throw new Error("Organization could not be created."); + } + const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`; const orgWithSameSlug = await prisma.organization.findFirst({ @@ -172,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) { return "prod"; } case "STAGING": { - return "staging"; + return "stg"; } case "PREVIEW": { - return "preview"; + return "prev"; } } } diff --git a/apps/webapp/app/models/project.server.ts b/apps/webapp/app/models/project.server.ts index f4fe0e4d7b..2c5b67d00a 100644 --- a/apps/webapp/app/models/project.server.ts +++ b/apps/webapp/app/models/project.server.ts @@ -65,6 +65,7 @@ export async function createProject( // Create the dev and prod environments await createEnvironment(organization, project, "PRODUCTION"); + await createEnvironment(organization, project, "STAGING"); for (const member of project.organization.members) { await createEnvironment(organization, project, "DEVELOPMENT", member); diff --git a/apps/webapp/app/models/runConnection.server.ts b/apps/webapp/app/models/runConnection.server.ts index 2ddd2bef56..732781ecc7 100644 --- a/apps/webapp/app/models/runConnection.server.ts +++ b/apps/webapp/app/models/runConnection.server.ts @@ -16,7 +16,7 @@ export async function resolveRunConnections( const result: Record = {}; for (const connection of connections) { - if (connection.integration.authSource === "LOCAL") { + if (connection.integration.authSource !== "HOSTED") { continue; } diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts index 19951eb6f7..b674dd0a42 100644 --- a/apps/webapp/app/models/task.server.ts +++ b/apps/webapp/app/models/task.server.ts @@ -1,5 +1,5 @@ import type { Task, TaskAttempt } from "@trigger.dev/database"; -import { ServerTask } from "@trigger.dev/core"; +import { CachedTask, ServerTask } from "@trigger.dev/core"; export type TaskWithAttempts = Task & { attempts: TaskAttempt[] }; @@ -23,5 +23,90 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask attempts: task.attempts.length, idempotencyKey: task.idempotencyKey, operation: task.operation, + callbackUrl: task.callbackUrl, }; } + +export type TaskForCaching = Pick< + Task, + "id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId" +>; + +export function prepareTasksForCaching( + possibleTasks: TaskForCaching[], + maxSize: number +): { + tasks: CachedTask[]; + cursor: string | undefined; +} { + const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop); + + // Select tasks using greedy approach + const tasksToRun: CachedTask[] = []; + let remainingSize = maxSize; + + for (const task of tasks) { + const cachedTask = prepareTaskForCaching(task); + const size = calculateCachedTaskSize(cachedTask); + + if (size <= remainingSize) { + tasksToRun.push(cachedTask); + remainingSize -= size; + } + } + + return { + tasks: tasksToRun, + cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined, + }; +} + +export function prepareTasksForCachingLegacy( + possibleTasks: TaskForCaching[], + maxSize: number +): { + tasks: CachedTask[]; + cursor: string | undefined; +} { + const tasks = possibleTasks.filter((task) => task.status === "COMPLETED"); + + // Prepare tasks and calculate their sizes + const availableTasks = tasks.map((task) => { + const cachedTask = prepareTaskForCaching(task); + return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) }; + }); + + // Sort tasks in ascending order by size + availableTasks.sort((a, b) => a.size - b.size); + + // Select tasks using greedy approach + const tasksToRun: CachedTask[] = []; + let remainingSize = maxSize; + + for (const { task, size } of availableTasks) { + if (size <= remainingSize) { + tasksToRun.push(task); + remainingSize -= size; + } + } + + return { + tasks: tasksToRun, + cursor: undefined, + }; +} + +function prepareTaskForCaching(task: TaskForCaching): CachedTask { + return { + id: task.idempotencyKey, // We should eventually move this back to task.id + status: task.status, + idempotencyKey: task.idempotencyKey, + noop: task.noop, + output: task.output as any, + parentId: task.parentId, + }; +} + +function calculateCachedTaskSize(task: CachedTask): number { + return JSON.stringify(task).length; +} diff --git a/apps/webapp/app/presenters/ApiRunPresenter.server.ts b/apps/webapp/app/presenters/ApiRunPresenter.server.ts new file mode 100644 index 0000000000..6e8ccd3d2b --- /dev/null +++ b/apps/webapp/app/presenters/ApiRunPresenter.server.ts @@ -0,0 +1,72 @@ +import { Job } from "@trigger.dev/database"; +import { PrismaClient, prisma } from "~/db.server"; + +type ApiRunOptions = { + runId: Job["id"]; + maxTasks?: number; + taskDetails?: boolean; + subTasks?: boolean; + cursor?: string; +}; + +export class ApiRunPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + runId, + maxTasks = 20, + taskDetails = false, + subTasks = false, + cursor, + }: ApiRunOptions) { + const take = Math.min(maxTasks, 50); + + return await prisma.jobRun.findUnique({ + where: { + id: runId, + }, + select: { + id: true, + status: true, + startedAt: true, + updatedAt: true, + completedAt: true, + environmentId: true, + output: true, + tasks: { + select: { + id: true, + parentId: true, + displayKey: true, + status: true, + name: true, + icon: true, + startedAt: true, + completedAt: true, + params: taskDetails, + output: taskDetails, + }, + where: { + parentId: subTasks ? undefined : null, + }, + orderBy: { + id: "asc", + }, + take: take + 1, + cursor: cursor + ? { + id: cursor, + } + : undefined, + }, + statuses: { + select: { key: true, label: true, state: true, data: true, history: true }, + }, + }, + }); + } +} diff --git a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts index bcb0629a70..477691f622 100644 --- a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts +++ b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts @@ -2,19 +2,19 @@ import { PrismaClient, prisma } from "~/db.server"; import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server"; import { Project } from "~/models/project.server"; import { User } from "~/models/user.server"; -import { +import type { Endpoint, EndpointIndex, RuntimeEnvironment, RuntimeEnvironmentType, -} from "../../../../packages/database/src"; -import { env } from "~/env.server"; +} from "@trigger.dev/database"; export type Client = { slug: string; endpoints: { DEVELOPMENT: ClientEndpoint; PRODUCTION: ClientEndpoint; + STAGING?: ClientEndpoint; }; }; @@ -133,6 +133,8 @@ export class EnvironmentsPresenter { throw new Error("Development environment not found, this should not happen"); } + const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING"); + const productionEnvironment = filtered.find( (environment) => environment.type === "PRODUCTION" ); @@ -151,6 +153,9 @@ export class EnvironmentsPresenter { state: "unconfigured", environment: productionEnvironment, }, + STAGING: stagingEnvironment + ? { state: "unconfigured", environment: stagingEnvironment } + : undefined, }, }; @@ -161,6 +166,16 @@ export class EnvironmentsPresenter { client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl); } + if (stagingEnvironment) { + const stagingEndpoint = stagingEnvironment.endpoints.find( + (endpoint) => endpoint.slug === slug + ); + + if (stagingEndpoint) { + client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl); + } + } + const prodEndpoint = productionEnvironment.endpoints.find( (endpoint) => endpoint.slug === slug ); diff --git a/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts index 3dea16ff00..901d79b171 100644 --- a/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts +++ b/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts @@ -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, }; diff --git a/apps/webapp/app/presenters/IntegrationsPresenter.server.ts b/apps/webapp/app/presenters/IntegrationsPresenter.server.ts index 45fe635476..c38449a175 100644 --- a/apps/webapp/app/presenters/IntegrationsPresenter.server.ts +++ b/apps/webapp/app/presenters/IntegrationsPresenter.server.ts @@ -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, diff --git a/apps/webapp/app/presenters/OrgUsagePresenter.server.ts b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts new file mode 100644 index 0000000000..7c08a24687 --- /dev/null +++ b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts @@ -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; +} diff --git a/apps/webapp/app/presenters/RunPresenter.server.ts b/apps/webapp/app/presenters/RunPresenter.server.ts index a3e380aa1a..34934647d0 100644 --- a/apps/webapp/app/presenters/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/RunPresenter.server.ts @@ -13,10 +13,11 @@ type RunOptions = { userId: string; }; -export type Run = NonNullable>>; -export type Task = NonNullable>>["tasks"][number]; -export type Event = NonNullable>>["event"]; +export type ViewRun = NonNullable>>; +export type ViewTask = NonNullable>>["tasks"][number]; +export type ViewEvent = NonNullable>>["event"]; +type QueryEvent = NonNullable>>["event"]; type QueryTask = NonNullable>>["tasks"][number]; export class RunPresenter { @@ -76,7 +77,7 @@ export class RunPresenter { type: run.environment.type, slug: run.environment.slug, }, - event: run.event, + event: this.#prepareEventData(run.event), tasks, runConnections: run.runConnections, missingConnections: run.missingConnections, @@ -84,6 +85,22 @@ export class RunPresenter { }; } + #prepareEventData(event: QueryEvent) { + return { + id: event.eventId, + name: event.name, + payload: JSON.stringify(event.payload), + context: JSON.stringify(event.context), + timestamp: event.timestamp, + deliveredAt: event.deliveredAt, + externalAccount: event.externalAccount + ? { + identifier: event.externalAccount.identifier, + } + : undefined, + }; + } + query({ id, userId }: RunOptions) { return this.#prismaClient.jobRun.findFirst({ select: { @@ -110,11 +127,17 @@ export class RunPresenter { }, event: { select: { - id: true, + eventId: true, name: true, payload: true, + context: true, timestamp: true, deliveredAt: true, + externalAccount: { + select: { + identifier: true, + }, + }, }, }, tasks: { diff --git a/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts b/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts index 1898594bfc..4d27df4b71 100644 --- a/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts +++ b/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts @@ -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, 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); + } } diff --git a/apps/webapp/app/presenters/TestJobPresenter.server.ts b/apps/webapp/app/presenters/TestJobPresenter.server.ts index 3e0a7a8fd6..c87b035c8d 100644 --- a/apps/webapp/app/presenters/TestJobPresenter.server.ts +++ b/apps/webapp/app/presenters/TestJobPresenter.server.ts @@ -4,6 +4,7 @@ import { PrismaClient, prisma } from "~/db.server"; import { Job } from "~/models/job.server"; import { Organization } from "~/models/organization.server"; import { Project } from "~/models/project.server"; +import { EventExample } from "@trigger.dev/core"; export class TestJobPresenter { #prismaClient: PrismaClient; @@ -39,6 +40,15 @@ export class TestJobPresenter { payload: true, }, }, + integrations: { + select: { + integration: { + select: { + authSource: true, + }, + }, + }, + }, }, }, environment: { @@ -58,14 +68,22 @@ export class TestJobPresenter { name: "latest", }, }, - _count: { + runs: { select: { - runs: { - where: { - isTest: true, + id: true, + createdAt: true, + number: true, + status: true, + event: { + select: { + payload: true, }, }, }, + orderBy: { + createdAt: "desc", + }, + take: 5, }, }, where: { @@ -88,6 +106,15 @@ export class TestJobPresenter { throw new Error("Job not found"); } + //collect together the examples, we don't care about the environments + const examples = job.aliases.flatMap((alias) => + alias.version.examples.map((example) => ({ + ...example, + icon: example.icon ?? undefined, + payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined, + })) + ); + return { environments: job.aliases.map((alias) => ({ id: alias.environment.id, @@ -95,12 +122,18 @@ export class TestJobPresenter { slug: alias.environment.slug, userId: alias.environment.orgMember?.userId, versionId: alias.version.id, - examples: alias.version.examples.map((example) => ({ - ...example, - payload: JSON.stringify(example.payload, exampleReplacer, 2), - })), + hasAuthResolver: alias.version.integrations.some( + (i) => i.integration.authSource === "RESOLVER" + ), + })), + examples, + runs: job.runs.map((r) => ({ + id: r.id, + number: r.number, + status: r.status, + created: r.createdAt, + payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined, })), - hasTestRuns: job._count.runs > 0, }; } } diff --git a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts index 127f734390..7a93e77ac9 100644 --- a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts +++ b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts @@ -17,16 +17,34 @@ export class TriggerDetailsPresenter { select: { event: { select: { - id: true, + eventId: true, name: true, payload: true, + context: true, timestamp: true, deliveredAt: true, + externalAccount: { + select: { + identifier: true, + }, + }, }, }, }, }); - return event; + return { + id: event.eventId, + name: event.name, + payload: JSON.stringify(event.payload, null, 2), + context: JSON.stringify(event.context, null, 2), + timestamp: event.timestamp, + deliveredAt: event.deliveredAt, + externalAccount: event.externalAccount + ? { + identifier: event.externalAccount.identifier, + } + : undefined, + }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx index 64c43ecfe5..e0ac76fe11 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx @@ -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) => { + if (active && payload) { + return ( +
+

{label}:

+

{payload[0].value}

+
+ ); + } + + return null; +}; export default function Page() { + const organization = useOrganization(); + const loaderData = useTypedLoaderData(); + return ( - +
+
+
+ Total Runs this month + +
+
+

{loaderData.runsCount.toLocaleString()}

+ + {loaderData.runsCountLastMonth} runs last month + +
+
+
+
+ Total Jobs + +
+
+

{loaderData.totalJobs.toLocaleString()}

+ + {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 + )} + +
+
+
+
+ Total Integrations + +
+
+

{loaderData.totalIntegrations.toLocaleString()}

+ + {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 + + )} + +
+
+
+
+ Team members + +
+
+

{loaderData.totalMembers.toLocaleString()}

+ + Manage + + +
+
+
+
+
+ Job Runs per month + + + + `${value}`} + /> + } /> + + + +
+
+
+ Jobs + Runs +
+
+ {loaderData.jobs.map((job) => ( + +
+

{job.slug}

+

Project: {job.project.name}

+
+
{job._count.runs.toLocaleString()}
+ + ))} +
+
+
); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx index d48861d176..0144db63bb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx @@ -104,6 +104,7 @@ export default function Page() { fullWidth={true} value={filterText} onChange={(e) => setFilterText(e.target.value)} + autoFocus />
@@ -160,7 +161,7 @@ function ExampleJobs() { height="250" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen - className="mb-4 w-full border-b border-slate-800" + className="mb-4 border-b border-slate-800" /> How to create a Job diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx index 7d3a2fcaa2..a335b14119 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx @@ -85,8 +85,8 @@ export default function Page() { const client = clients.find((c) => c.slug === selected.client); if (!client) return undefined; - if (selected.type === "PREVIEW" || selected.type === "STAGING") { - throw new Error("PREVIEW/STAGING is not yet supported"); + if (selected.type === "PREVIEW") { + throw new Error("PREVIEW is not yet supported"); } return { @@ -195,6 +195,18 @@ export default function Page() { }) } /> + {client.endpoints.STAGING && ( + + setSelected({ + client: client.slug, + type: "STAGING", + }) + } + /> + )} )}
- {selectedEndpoint && ( + {selectedEndpoint && selectedEndpoint.endpoint && ( - } @@ -221,10 +224,12 @@ function PossibleIntegrationsList({ Create an Integration -
@@ -482,77 +487,16 @@ function AddIntegrationConnection({ icon?: string; }) { return ( -
- - - {name} - -
- {isIntegration && } - -
-
- ); -} - -function ExternalIntegrationLink({ - name, - label, - trailingIcon, -}: { - name: string; - label: string; - trailingIcon: string; -}) { - return ( - - - - {label} - -
- -
-
+ ); } export function IntegrationIcon() { return ; } - -function InfoLink({ text }: { text: string }) { - return ( -
- - - {text} - -
- -
-
- ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx index 37cca5fa2e..f3dc9a36f0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx @@ -1,6 +1,7 @@ import { useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { PopoverTrigger } from "@radix-ui/react-popover"; +import { ClipboardIcon } from "@heroicons/react/20/solid"; +import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline"; import { Form, useActionData, useSubmit } from "@remix-run/react"; import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime"; import { useCallback, useRef, useState } from "react"; @@ -8,13 +9,16 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { HowToRunATest } from "~/components/helpContent/HelpContentText"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; -import { Button, ButtonContent } from "~/components/primitives/Buttons"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; +import { DateTime } from "~/components/primitives/DateTime"; +import { DetailCell } from "~/components/primitives/DetailCell"; import { FormError } from "~/components/primitives/FormError"; -import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; -import { Popover, PopoverContent } from "~/components/primitives/Popover"; +import { Header2 } from "~/components/primitives/Headers"; +import { Hint } from "~/components/primitives/Hint"; +import { Input } from "~/components/primitives/Input"; +import { InputGroup } from "~/components/primitives/InputGroup"; import { Select, SelectContent, @@ -23,6 +27,8 @@ import { SelectTrigger, SelectValue, } from "~/components/primitives/Select"; +import { TextLink } from "~/components/primitives/TextLink"; +import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses"; import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { TestJobPresenter } from "~/presenters/TestJobPresenter.server"; import { TestJobService } from "~/services/jobs/testJob.server"; @@ -36,14 +42,14 @@ export const loader = async ({ request, params }: LoaderArgs) => { const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params); const presenter = new TestJobPresenter(); - const { environments, hasTestRuns } = await presenter.call({ + const { environments, runs, examples } = await presenter.call({ userId, organizationSlug, projectSlug: projectParam, jobSlug: jobParam, }); - return typedjson({ environments, hasTestRuns }); + return typedjson({ environments, runs, examples }); }; const schema = z.object({ @@ -69,6 +75,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 +91,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( @@ -116,22 +119,31 @@ export const handle: Handle = { const startingJson = "{\n\n}"; export default function Page() { + const { environments, runs, examples } = useTypedLoaderData(); + + //form submission const submit = useSubmit(); const lastSubmission = useActionData(); - const [isExamplePopoverOpen, setIsExamplePopoverOpen] = useState(false); - const { environments, hasTestRuns } = useTypedLoaderData(); - - const [defaultJson, setDefaultJson] = useState(startingJson); - const currentJson = useRef(defaultJson); - const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments[0].id); - const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId); + //examples + const [selectedCodeSampleId, setSelectedCodeSampleId] = useState( + examples.at(0)?.id ?? runs.at(0)?.id + ); + const selectedCodeSample = + examples.find((e) => e.id === selectedCodeSampleId)?.payload ?? + runs.find((r) => r.id === selectedCodeSampleId)?.payload; - const insertCode = useCallback((code: string) => { + const [defaultJson, setDefaultJson] = useState(selectedCodeSample ?? startingJson); + const setCode = useCallback((code: string) => { setDefaultJson(code); - setIsExamplePopoverOpen(false); }, []); + const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments[0].id); + const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId); + + const currentJson = useRef(defaultJson); + const [currentAccountId, setCurrentAccountId] = useState(undefined); + const submitForm = useCallback( (e: React.FormEvent) => { submit( @@ -139,6 +151,7 @@ export default function Page() { payload: currentJson.current, environmentId: selectedEnvironmentId, versionId: selectedEnvironment?.versionId ?? "", + ...(currentAccountId ? { accountId: currentAccountId } : {}), }, { action: "", @@ -147,10 +160,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 }) { @@ -168,103 +181,178 @@ export default function Page() { } return ( - - {(open) => ( -
-
-
submitForm(e)} - > -
-
- - - +
+
+ submitForm(e)} + > +
+
+ { + currentJson.current = v; - {selectedEnvironment && selectedEnvironment.examples.length > 0 && ( - setIsExamplePopoverOpen(open)} + //deselect the example if it's been edited + if (selectedCodeSampleId) { + if (v !== selectedCodeSample) { + setDefaultJson(v); + setSelectedCodeSampleId(undefined); + } + } + }} + height="100%" + min-height="100%" + max-height="100%" + autoFocus + placeholder="Use your schema to enter valid JSON or add one of the example payloads then click 'Run test'" + className="h-full" + /> +
+
+ {examples.length > 0 && ( +
+ Example payloads + {examples.map((example) => ( + - ))} - - - )} + + + ))}
- -
-
- (currentJson.current = v)} - minHeight="150px" - /> -
-
- {payload.error ? ( - {payload.error} + )} +
+ Recent payloads + {runs.length === 0 ? ( + + Recent payloads will show here once you've completed a Run. + ) : ( -
+
+ {runs.map((run) => ( + + ))} +
)} -
- + + {selectedEnvironment?.hasAuthResolver && ( +
+ Account ID + + setCurrentAccountId(e.target.value)} + /> + {accountId.error} + + Learn about testing Jobs with an Account ID in our{" "} + + BYOAuth docs + + + +
+ )} +
+
+
+ + Learn more about running tests + +
+ {payload.error ? ( + {payload.error} + ) : ( +
+ )} + + + + +
- - - -
- )} - + +
+
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx index 10bd0727c3..591e2ce879 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx @@ -39,9 +39,14 @@ export default function SetUpAstro() { useProjectSetupComplete(); const devEnvironment = useDevEnvironment(); invariant(devEnvironment, "Dev environment must be defined"); + const appOrigin = useAppOrigin(); + return (
+
+ +
Get setup in 5 minutes @@ -76,28 +81,16 @@ export default function SetUpAstro() {
- - Copy your server API Key to your clipboard: -
- Server} - /> -
- Now follow this guide: - - Manual installation guide - -
+ + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple + example Job in example.ts to help you + get started. + diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx index f500087cd6..7823cf9143 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx @@ -1,21 +1,120 @@ +import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; +import invariant from "tiny-invariant"; import { ExpressLogo } from "~/assets/logos/ExpressLogo"; -import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; +import { Feedback } from "~/components/Feedback"; +import { PageGradient } from "~/components/PageGradient"; +import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; +import { StepContentContainer } from "~/components/StepContentContainer"; +import { InlineCode } from "~/components/code/InlineCode"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { Badge } from "~/components/primitives/Badge"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; +import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { Header1 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { StepNumber } from "~/components/primitives/StepNumber"; +import { useAppOrigin } from "~/hooks/useAppOrigin"; +import { useDevEnvironment } from "~/hooks/useEnvironments"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { Handle } from "~/utils/handle"; -import { trimTrailingSlash } from "~/utils/pathBuilder"; +import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; export const handle: Handle = { breadcrumb: (match) => , }; 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 ( - - - + +
+
+ +
+
+ + Get setup in 5 minutes + +
+ + Choose a different framework + + + I'm stuck! + + } + defaultValue="help" + /> +
+
+
+ + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + +
+ + + Copy your server API Key to your clipboard: +
+ Server} + /> +
+ Now follow this guide: + + Manual installation guide + +
+ + + + + You may be using the `start` script instead, in which case substitute `dev` in the + above commands. + + + + + + + + + This page will automatically refresh. + +
+
+
+
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx index a9eaa74691..d289e0ff7f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx @@ -1,21 +1,179 @@ -import { FastifyLogo } from "~/assets/logos/FastifyLogo"; -import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { useState } from "react"; +import invariant from "tiny-invariant"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; +import { useDevEnvironment } from "~/hooks/useEnvironments"; +import { useAppOrigin } from "~/hooks/useAppOrigin"; +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 { StepNumber } from "~/components/primitives/StepNumber"; +import { StepContentContainer } from "~/components/StepContentContainer"; +import { RunDevCommand, TriggerDevStep, InitCommand } from "~/components/SetupCommands"; +import { Header1 } from "~/components/primitives/Headers"; +import { LinkButton } from "~/components/primitives/Buttons"; +import { PageGradient } from "~/components/PageGradient"; +import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { Button } from "~/components/primitives/Buttons"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { InlineCode } from "~/components/code/InlineCode"; +import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { CodeBlock } from "~/components/code/CodeBlock"; +import { + ClientTabs, + ClientTabsList, + ClientTabsTrigger, + ClientTabsContent, +} from "~/components/primitives/ClientTabs"; export const handle: Handle = { - breadcrumb: (match) => , + breadcrumb: (match) => , }; export default function Page() { + useProjectSetupComplete(); + const devEnvironment = useDevEnvironment(); + + invariant(devEnvironment, "devEnvironment is required"); + return ( - - - + +
+ + Get setup in 5 minutes for an existing Fastify project + + + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + + + + + + npm + pnpm + yarn + + + + + + + + + + + + + + + + + + + { + await io.logger.info('Hello world!', { payload }); + + return { + message: 'Hello world!', + }; + }, +}); + `} + /> + + Replace "my-app" with an appropriate identifier for your project. + + + + + { + console.log("Listening on port 3000"); +});`} + /> + + + + + + + + + + This page will automatically refresh. + +
+
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx index 255f31340d..d13b547a03 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx @@ -1,21 +1,220 @@ -import { NestjsLogo } from "~/assets/logos/NestjsLogo"; -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 { StepContentContainer } from "~/components/StepContentContainer"; +import { InlineCode } from "~/components/code/InlineCode"; +import { InstallPackages } from "~/components/code/InstallPackages"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +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"; +import { CodeBlock } from "../../components/code/CodeBlock"; +import { TriggerDevStep } from "~/components/SetupCommands"; export const handle: Handle = { - breadcrumb: (match) => , + breadcrumb: (match) => , }; -export default function Page() { +const AppModuleCode = ` +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TriggerDevModule } from '@trigger.dev/nestjs'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TriggerDevModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + id: 'my-nest-app', + apiKey: config.getOrThrow('TRIGGER_API_KEY'), + apiUrl: config.getOrThrow('TRIGGER_API_URL'), + verbose: false, + ioLogLocalEnabled: true, + }), + }), + ], +}) +export class AppModule {} +`; + +const JobControllerCode = ` +import { Controller, Get } from '@nestjs/common'; +import { InjectTriggerDevClient } from '@trigger.dev/nestjs'; +import { eventTrigger, TriggerClient } from '@trigger.dev/sdk'; + +@Controller() +export class JobController { + constructor( + @InjectTriggerDevClient() private readonly client: TriggerClient, + ) { + this.client.defineJob({ + id: 'test-job', + name: 'Test Job One', + version: '0.0.1', + trigger: eventTrigger({ + name: 'test.event', + }), + run: async (payload, io, ctx) => { + await io.logger.info('Hello world!', { payload }); + + return { + message: 'Hello world!', + }; + }, + }); + } + + @Get() + getHello(): string { + return \`Running Trigger.dev with client-id \${this.client.id}\`; + } +}`; + +const AppModuleWithControllerCode = ` +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TriggerDevModule } from '@trigger.dev/nestjs'; +import { JobController } from './job.controller'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TriggerDevModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + id: 'my-nest-app', + apiKey: config.getOrThrow('TRIGGER_API_KEY'), + apiUrl: config.getOrThrow('TRIGGER_API_URL'), + verbose: false, + ioLogLocalEnabled: true, + }), + }), + ], + controllers: [ + //...existingControllers, + JobController + ], +}) +export class AppModule {} +`; + +const packageJsonCode = `"trigger.dev": { + "endpointId": "my-nest-app" +}`; + +export default function SetupNestJS() { + const organization = useOrganization(); + const project = useProject(); + useProjectSetupComplete(); + const devEnvironment = useDevEnvironment(); + const appOrigin = useAppOrigin(); + + invariant(devEnvironment, "devEnvironment is required"); + return ( - - - + +
+
+ + Get setup in 2 minutes + +
+ + Choose a different framework + + + I'm stuck! + + } + defaultValue="help" + /> +
+
+ <> + + + + + + + + Inside your .env file, create the following env variables: + + + + + + + Now, go to your app.module.ts and add the{" "} + TriggerDevModule: + + + + + + + Create a controller called{" "} + job.controller.ts and add the following code: + + + + + + + Now, add the new controller to your{" "} + app.module.ts: + + + + + + + Now, add this to the top-level of your package.json: + + + + + + + Finally, run your project with npm run start: + + + + + + + + + This page will automatically refresh. + + +
+
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx index df31e16695..9cb4f31752 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx @@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject"; import { Handle } from "~/utils/handle"; import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; import { Callout } from "~/components/primitives/Callout"; +import { NextjsLogo } from "~/assets/logos/NextjsLogo"; type SelectionChoices = "use-existing-project" | "create-new-next-app"; @@ -48,6 +49,9 @@ export default function SetupNextjs() { return (
+
+ +
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx index f6bf7ad037..12a9397ca4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx @@ -25,8 +25,9 @@ import { useProject } from "~/hooks/useProject"; import { Handle } from "~/utils/handle"; import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; import { Callout } from "~/components/primitives/Callout"; -import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; +import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; import { Badge } from "~/components/primitives/Badge"; +import { RemixLogo } from "~/assets/logos/RemixLogo"; export const handle: Handle = { breadcrumb: (match) => , @@ -38,9 +39,14 @@ export default function SetUpRemix() { useProjectSetupComplete(); const devEnvironment = useDevEnvironment(); invariant(devEnvironment, "Dev environment must be defined"); + const appOrigin = useAppOrigin(); + return (
+
+ +
Get setup in 5 minutes @@ -75,28 +81,16 @@ export default function SetUpRemix() {
- - Copy your server API Key to your clipboard: -
- Server} - /> -
- Now follow this guide: - - Manual installation guide - -
+ + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple + example Job in example.server.ts to + help you get started. + diff --git a/apps/webapp/app/routes/api.v1.accounts.$accountId.connections.$clientSlug.ts b/apps/webapp/app/routes/api.v1.accounts.$accountId.connections.$clientSlug.ts index 5763ce8a37..e9f8d39b6f 100644 --- a/apps/webapp/app/routes/api.v1.accounts.$accountId.connections.$clientSlug.ts +++ b/apps/webapp/app/routes/api.v1.accounts.$accountId.connections.$clientSlug.ts @@ -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({ diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts new file mode 100644 index 0000000000..876d22d846 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts @@ -0,0 +1,71 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { PrismaErrorSchema } from "~/db.server"; +import { z } from "zod"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { CancelRunService } from "~/services/runs/cancelRun.server"; +import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; + +const ParamsSchema = z.object({ + runId: z.string(), +}); + +export async function action({ request, params }: ActionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + // Authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API Key" }, { status: 401 }); + } + + const parsed = ParamsSchema.safeParse(params); + + if (!parsed.success) { + return json({ error: "Invalid or Missing runId" }, { status: 400 }); + } + + const { runId } = parsed.data; + + const service = new CancelRunService(); + try { + await service.call({ runId }); + } catch (error) { + const prismaError = PrismaErrorSchema.safeParse(error); + // Record not found in the database + if (prismaError.success && prismaError.data.code === "P2005") { + return json({ error: "Run not found" }, { status: 404 }); + } else { + return json({ error: "Internal Server Error" }, { status: 500 }); + } + } + + const presenter = new ApiRunPresenter(); + const jobRun = await presenter.call({ + runId: runId, + }); + + if (!jobRun) { + return json({ message: "Run not found" }, { status: 404 }); + } + + return json({ + id: jobRun.id, + status: jobRun.status, + startedAt: jobRun.startedAt, + updatedAt: jobRun.updatedAt, + completedAt: jobRun.completedAt, + output: jobRun.output, + tasks: jobRun.tasks, + statuses: jobRun.statuses.map((s) => ({ + ...s, + state: s.state ?? undefined, + data: s.data ?? undefined, + history: s.history ?? undefined, + })), + }); +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts b/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts new file mode 100644 index 0000000000..e547534231 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts @@ -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; + } +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.statuses.ts b/apps/webapp/app/routes/api.v1.runs.$runId.statuses.ts new file mode 100644 index 0000000000..f7e6351de2 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.statuses.ts @@ -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 })); + } +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts new file mode 100644 index 0000000000..5c1425e2a0 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.callback.$secret.ts @@ -0,0 +1,124 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { z } from "zod"; +import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server"; +import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server"; +import { logger } from "~/services/logger.server"; + +const ParamsSchema = z.object({ + runId: z.string(), + id: z.string(), + secret: z.string(), +}); + +export async function action({ request, params }: ActionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + const { runId, id } = ParamsSchema.parse(params); + + // Parse body as JSON (no schema parsing) + const body = await request.json(); + + const service = new CallbackRunTaskService(); + + try { + // Complete task with request body as output + await service.call(runId, id, body, request.url); + + return json({ success: true }); + } catch (error) { + if (error instanceof Error) { + logger.error("Error while processing task callback:", { error }); + } + + return json({ error: "Something went wrong" }, { status: 500 }); + } +} + +export class CallbackRunTaskService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(runId: string, id: string, taskBody: any, callbackUrl: string): Promise { + const task = await findTask(prisma, id); + + if (!task) { + return; + } + + if (task.runId !== runId) { + return; + } + + if (task.status !== "WAITING") { + return; + } + + if (!task.callbackUrl) { + return; + } + + if (new URL(task.callbackUrl).pathname !== new URL(callbackUrl).pathname) { + logger.error("Callback URLs don't match", { runId, taskId: id, callbackUrl }); + return; + } + + logger.debug("CallbackRunTaskService.call()", { task }); + + await this.#resumeTask(task, taskBody); + } + + async #resumeTask(task: NonNullable, output: any) { + await $transaction(this.#prismaClient, async (tx) => { + await tx.taskAttempt.updateMany({ + where: { + taskId: task.id, + status: "PENDING", + }, + data: { + status: "COMPLETED", + }, + }); + + await tx.task.update({ + where: { id: task.id }, + data: { + status: "COMPLETED", + completedAt: new Date(), + output: output ? output : undefined, + }, + }); + + await this.#resumeRunExecution(task, tx); + }); + } + + async #resumeRunExecution(task: NonNullable, prisma: PrismaClientOrTransaction) { + await enqueueRunExecutionV2(task.run, prisma, { + skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + }); + } +} + +type FoundTask = Awaited>; + +async function findTask(prisma: PrismaClientOrTransaction, id: string) { + return prisma.task.findUnique({ + where: { id }, + include: { + run: { + include: { + environment: true, + queue: true, + }, + }, + }, + }); +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts index 4b3d201093..f116261cb3 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts @@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime"; import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core"; import { CompleteTaskBodyInputSchema } from "@trigger.dev/core"; import { z } from "zod"; -import { $transaction, PrismaClient, prisma } from "~/db.server"; +import { PrismaClient, prisma } from "~/db.server"; import { taskWithAttemptsToServerTask } from "~/models/task.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; @@ -86,8 +86,8 @@ export class CompleteRunTaskService { ): Promise { // Using a transaction, we'll first check to see if the task already exists and return if if it does // If it doesn't exist, we'll create it and return it - const task = await this.#prismaClient.$transaction(async (prisma) => { - const existingTask = await prisma.task.findUnique({ + const task = await this.#prismaClient.$transaction(async (tx) => { + const existingTask = await tx.task.findUnique({ where: { id, }, @@ -129,35 +129,31 @@ export class CompleteRunTaskService { return existingTask; } - const task = await $transaction(prisma, async (tx) => { - if (existingTask.attempts.length === 1) { - await tx.taskAttempt.update({ - where: { - id: existingTask.attempts[0].id, - }, - data: { - status: "COMPLETED", - }, - }); - } - - return await tx.task.update({ + if (existingTask.attempts.length === 1) { + await tx.taskAttempt.update({ where: { - id, + id: existingTask.attempts[0].id, }, data: { status: "COMPLETED", - output: taskBody.output ?? undefined, - completedAt: new Date(), - outputProperties: taskBody.properties, - }, - include: { - attempts: true, }, }); - }); + } - return task; + return await tx.task.update({ + where: { + id, + }, + data: { + status: "COMPLETED", + output: taskBody.output ?? undefined, + completedAt: new Date(), + outputProperties: taskBody.properties, + }, + include: { + attempts: true, + }, + }); }); return task ? taskWithAttemptsToServerTask(task) : undefined; diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts index e0ddb26dd2..8bdfb4f44d 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts @@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core"; import { z } from "zod"; -import { $transaction, PrismaClient, prisma } from "~/db.server"; +import { PrismaClient, prisma } from "~/db.server"; import { taskWithAttemptsToServerTask } from "~/models/task.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; @@ -86,8 +86,8 @@ export class FailRunTaskService { ): Promise { // Using a transaction, we'll first check to see if the task already exists and return if if it does // If it doesn't exist, we'll create it and return it - const task = await this.#prismaClient.$transaction(async (prisma) => { - const existingTask = await prisma.task.findUnique({ + const task = await this.#prismaClient.$transaction(async (tx) => { + const existingTask = await tx.task.findUnique({ where: { id, }, @@ -129,35 +129,31 @@ export class FailRunTaskService { return existingTask; } - const task = await $transaction(prisma, async (tx) => { - if (existingTask.attempts.length === 1) { - await tx.taskAttempt.update({ - where: { - id: existingTask.attempts[0].id, - }, - data: { - status: "ERRORED", - error: formatError(taskBody.error), - }, - }); - } - - return await prisma.task.update({ + if (existingTask.attempts.length === 1) { + await tx.taskAttempt.update({ where: { - id, + id: existingTask.attempts[0].id, }, data: { status: "ERRORED", - output: taskBody.error ?? undefined, - completedAt: new Date(), - }, - include: { - attempts: true, + error: formatError(taskBody.error), }, }); - }); + } - return task; + return await tx.task.update({ + where: { + id, + }, + data: { + status: "ERRORED", + output: taskBody.error ?? undefined, + completedAt: new Date(), + }, + include: { + attempts: true, + }, + }); }); return task ? taskWithAttemptsToServerTask(task) : undefined; diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 0e5eaecd99..94a6ba8caf 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -1,14 +1,22 @@ 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 } from "@trigger.dev/core"; +import { + API_VERSIONS, + RunTaskBodyOutput, + RunTaskBodyOutputSchema, + RunTaskResponseWithCachedTasksBody, + ServerTask, +} from "@trigger.dev/core"; import { z } from "zod"; import { $transaction, PrismaClient, prisma } from "~/db.server"; -import { taskWithAttemptsToServerTask } from "~/models/task.server"; +import { prepareTasksForCaching, 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 { generateSecret } from "~/services/sources/utils.server"; +import { env } from "~/env.server"; const ParamsSchema = z.object({ runId: z.string(), @@ -16,6 +24,8 @@ const ParamsSchema = z.object({ const HeadersSchema = z.object({ "idempotency-key": z.string(), + "trigger-version": z.string().optional().nullable(), + "x-cached-tasks-cursor": z.string().optional().nullable(), }); export async function action({ request, params }: ActionArgs) { @@ -37,7 +47,11 @@ export async function action({ request, params }: ActionArgs) { return json({ error: "Invalid or Missing idempotency key" }, { status: 400 }); } - const { "idempotency-key": idempotencyKey } = headers.data; + const { + "idempotency-key": idempotencyKey, + "trigger-version": triggerVersion, + "x-cached-tasks-cursor": cachedTasksCursor, + } = headers.data; const { runId } = ParamsSchema.parse(params); @@ -48,6 +62,8 @@ export async function action({ request, params }: ActionArgs) { body: anyBody, runId, idempotencyKey, + triggerVersion, + cachedTasksCursor, }); const body = RunTaskBodyOutputSchema.safeParse(anyBody); @@ -71,6 +87,26 @@ export async function action({ request, params }: ActionArgs) { return json({ error: "Something went wrong" }, { status: 500 }); } + if (triggerVersion === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) { + const requestMigration = new ChangeRequestLazyLoadedCachedTasks(); + + const responseBody = await requestMigration.call(runId, task, cachedTasksCursor); + + logger.debug( + "RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks", + { + responseBody, + cachedTasksCursor, + } + ); + + return json(responseBody, { + headers: { + "trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, + }, + }); + } + return json(task); } catch (error) { if (error instanceof Error) { @@ -81,6 +117,51 @@ export async function action({ request, params }: ActionArgs) { } } +class ChangeRequestLazyLoadedCachedTasks { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call( + runId: string, + task: ServerTask, + cursor?: string | null + ): Promise { + if (!cursor) { + return { + task, + }; + } + + // We need to limit the cached tasks to not be too large >2MB when serialized + const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000; + + const nextTasks = await this.#prismaClient.task.findMany({ + where: { + runId, + status: "COMPLETED", + noop: false, + }, + take: 250, + cursor: { + id: cursor, + }, + orderBy: { + id: "asc", + }, + }); + + const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT); + + return { + task, + cachedTasks: preparedTasks, + }; + } +} + export class RunTaskService { #prismaClient: PrismaClient; @@ -106,10 +187,13 @@ export class RunTaskService { }, }); + const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now(); + const callbackEnabled = taskBody.callback?.enabled; + if (existingTask) { if (existingTask.status === "CANCELED") { const existingTaskStatus = - (taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger + delayUntilInFuture || callbackEnabled || taskBody.trigger ? "WAITING" : taskBody.noop ? "COMPLETED" @@ -154,16 +238,21 @@ export class RunTaskService { status = "CANCELED"; } else { status = - (taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger + delayUntilInFuture || callbackEnabled || taskBody.trigger ? "WAITING" : taskBody.noop ? "COMPLETED" : "RUNNING"; } + const taskId = ulid(); + const callbackUrl = callbackEnabled + ? `${env.APP_ORIGIN}/api/v1/runs/${runId}/tasks/${taskId}/callback/${generateSecret(12)}` + : undefined; + const task = await tx.task.create({ data: { - id: ulid(), + id: taskId, idempotencyKey, displayKey: taskBody.displayKey, runConnection: taskBody.connectionKey @@ -194,6 +283,7 @@ export class RunTaskService { properties: taskBody.properties ?? undefined, redact: taskBody.redact ?? undefined, operation: taskBody.operation, + callbackUrl, style: taskBody.style ?? { style: "normal" }, attempts: { create: { @@ -217,6 +307,17 @@ export class RunTaskService { }, { tx, runAt: task.delayUntil ?? undefined } ); + } else if (task.status === "WAITING" && callbackUrl && taskBody.callback) { + if (taskBody.callback.timeoutInSeconds > 0) { + // We need to schedule the callback timeout + await workerQueue.enqueue( + "processCallbackTimeout", + { + id: task.id, + }, + { tx, runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000) } + ); + } } return task; diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.ts index 973d71f787..12fbaffc9a 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.ts @@ -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 { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; import { apiCors } from "~/utils/apiCors"; import { taskListToTree } from "~/utils/taskListToTree"; @@ -52,48 +51,15 @@ export async function loader({ request, params }: LoaderArgs) { const query = parsedQuery.data; const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE"; - const take = Math.min(query.take, 50); - const jobRun = await prisma.jobRun.findUnique({ - where: { - id: runId, - }, - select: { - id: true, - status: true, - startedAt: true, - updatedAt: true, - completedAt: true, - environmentId: true, - output: true, - tasks: { - select: { - id: true, - parentId: true, - displayKey: true, - status: true, - name: true, - icon: true, - startedAt: true, - completedAt: true, - params: showTaskDetails, - output: showTaskDetails, - }, - where: { - parentId: query.subtasks ? undefined : null, - }, - orderBy: { - id: "asc", - }, - take: take + 1, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, - }, - }, + const presenter = new ApiRunPresenter(); + const jobRun = await presenter.call({ + runId: runId, + maxTasks: take, + taskDetails: showTaskDetails, + subTasks: query.subtasks, + cursor: query.cursor, }); if (!jobRun) { @@ -122,6 +88,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, }) ); diff --git a/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts b/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts index 0412919ecf..ece220cb72 100644 --- a/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts +++ b/apps/webapp/app/routes/resources.projects.$projectId.endpoint.ts @@ -2,28 +2,15 @@ import { parse } from "@conform-to/zod"; import { ActionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { - CreateEndpointError, - CreateEndpointService, -} from "~/services/endpoints/createEndpoint.server"; -import { requireUserId } from "~/services/session.server"; -import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core"; -import { env } from "process"; +import { CreateEndpointError } from "~/services/endpoints/createEndpoint.server"; import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server"; -const ParamsSchema = z.object({ - projectId: z.string(), -}); - export const bodySchema = z.object({ environmentId: z.string(), url: z.string().url("Must be a valid URL"), }); -export async function action({ request, params }: ActionArgs) { - const userId = await requireUserId(request); - const { projectId } = ParamsSchema.parse(params); - +export async function action({ request }: ActionArgs) { const formData = await request.formData(); const submission = parse(formData, { schema: bodySchema }); @@ -48,7 +35,7 @@ export async function action({ request, params }: ActionArgs) { } const service = new ValidateCreateEndpointService(); - const result = await service.call({ + await service.call({ url: submission.value.url, environment, }); diff --git a/apps/webapp/app/services/endpointApi.server.ts b/apps/webapp/app/services/endpointApi.server.ts index 8cce435fa0..494930a073 100644 --- a/apps/webapp/app/services/endpointApi.server.ts +++ b/apps/webapp/app/services/endpointApi.server.ts @@ -1,7 +1,9 @@ import { + API_VERSIONS, ApiEventLog, DeliverEventResponseSchema, DeserializedJson, + EndpointHeadersSchema, ErrorWithStackSchema, HttpSourceRequest, HttpSourceResponseSchema, @@ -89,6 +91,15 @@ export class EndpointApi { }; } + const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries())); + + if (headers.success && headers.data["trigger-version"]) { + return { + ...pongResponse.data, + triggerVersion: headers.data["trigger-version"], + }; + } + return pongResponse.data; } @@ -129,41 +140,15 @@ export class EndpointApi { const anyBody = await response.json(); const data = IndexEndpointResponseSchema.parse(anyBody); + const headers = EndpointHeadersSchema.parse(Object.fromEntries(response.headers.entries())); return { ok: true, data, + headers, } as const; } - async deliverEvent(event: ApiEventLog) { - const response = await safeFetch(this.url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-trigger-api-key": this.apiKey, - "x-trigger-action": "DELIVER_EVENT", - }, - body: JSON.stringify(event), - }); - - if (!response) { - throw new Error(`Could not connect to endpoint ${this.url}`); - } - - if (!response.ok) { - throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`); - } - - const anyBody = await response.json(); - - logger.debug("deliverEvent() response from endpoint", { - body: anyBody, - }); - - return DeliverEventResponseSchema.parse(anyBody); - } - async executeJobRequest(options: RunJobBody) { const startTimeInMs = performance.now(); @@ -338,6 +323,15 @@ export class EndpointApi { }; } + const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries())); + + if (headers.success && headers.data["trigger-version"]) { + return { + ...validateResponse.data, + triggerVersion: headers.data["trigger-version"], + }; + } + return validateResponse.data; } } @@ -359,6 +353,7 @@ function addStandardRequestOptions(options: RequestInit) { headers: { ...options.headers, "user-agent": "triggerdotdev-server/2.0.0", + "x-trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, }, }; } diff --git a/apps/webapp/app/services/endpoints/createEndpoint.server.ts b/apps/webapp/app/services/endpoints/createEndpoint.server.ts index bd1839222f..20c62e7ef2 100644 --- a/apps/webapp/app/services/endpoints/createEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/createEndpoint.server.ts @@ -74,9 +74,11 @@ export class CreateEndpointService { slug: id, url: endpointUrl, indexingHookIdentifier: indexingHookIdentifier(), + version: pong.triggerVersion, }, update: { url: endpointUrl, + version: pong.triggerVersion, }, }); diff --git a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts index bc529fe0cf..2e6da01825 100644 --- a/apps/webapp/app/services/endpoints/indexEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/indexEndpoint.server.ts @@ -41,6 +41,7 @@ export class IndexEndpointService { } const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data; + const { "trigger-version": triggerVersion } = indexResponse.headers; logger.debug("Indexing endpoint", { endpointId: endpoint.id, @@ -48,6 +49,7 @@ export class IndexEndpointService { endpointSlug: endpoint.slug, source: source, sourceData: sourceData, + triggerVersion, stats: { jobs: jobs.length, sources: sources.length, @@ -56,6 +58,17 @@ export class IndexEndpointService { }, }); + if (triggerVersion && triggerVersion !== endpoint.version) { + await this.#prismaClient.endpoint.update({ + where: { + id: endpoint.id, + }, + data: { + version: triggerVersion, + }, + }); + } + const indexStats = { jobs: 0, sources: 0, diff --git a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts index d578a26d9d..4bbe5231fc 100644 --- a/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts +++ b/apps/webapp/app/services/endpoints/validateCreateEndpoint.server.ts @@ -58,9 +58,11 @@ export class ValidateCreateEndpointService { slug: validationResult.endpointId, url: endpointUrl, indexingHookIdentifier: indexingHookIdentifier(), + version: validationResult.triggerVersion, }, update: { url: endpointUrl, + version: validationResult.triggerVersion, }, }); diff --git a/apps/webapp/app/services/events/ingestSendEvent.server.ts b/apps/webapp/app/services/events/ingestSendEvent.server.ts index a4130bfdbc..b1de0e2a9e 100644 --- a/apps/webapp/app/services/events/ingestSendEvent.server.ts +++ b/apps/webapp/app/services/events/ingestSendEvent.server.ts @@ -7,10 +7,7 @@ import { logger } from "../logger.server"; export class IngestSendEvent { #prismaClient: PrismaClientOrTransaction; - constructor( - prismaClient: PrismaClientOrTransaction = prisma, - private deliverEvents = true - ) { + constructor(prismaClient: PrismaClientOrTransaction = prisma, private deliverEvents = true) { this.#prismaClient = prismaClient; } @@ -37,71 +34,55 @@ export class IngestSendEvent { try { const deliverAt = this.#calculateDeliverAt(options); - return await $transaction( - this.#prismaClient, - async (tx) => { - const externalAccount = options?.accountId - ? await tx.externalAccount.findUniqueOrThrow({ - where: { - environmentId_identifier: { - environmentId: environment.id, - identifier: options.accountId, - }, - }, - }) - : undefined; - - // Create a new event in the database - const eventLog = await tx.eventRecord.create({ - data: { - organization: { - connect: { - id: environment.organizationId, + return await $transaction(this.#prismaClient, async (tx) => { + const externalAccount = options?.accountId + ? await tx.externalAccount.upsert({ + where: { + environmentId_identifier: { + environmentId: environment.id, + identifier: options.accountId, }, }, - project: { - connect: { - id: environment.projectId, - }, + create: { + environmentId: environment.id, + organizationId: environment.organizationId, + identifier: options.accountId, }, - environment: { - connect: { - id: environment.id, - }, - }, - eventId: event.id, - name: event.name, - timestamp: event.timestamp ?? new Date(), - payload: event.payload ?? {}, - context: event.context ?? {}, - source: event.source ?? "trigger.dev", - sourceContext, - deliverAt: deliverAt, - externalAccount: externalAccount - ? { - connect: { - id: externalAccount.id, - }, - } - : {}, - }, - }); + update: {}, + }) + : undefined; - if (this.deliverEvents) { - // Produce a message to the event bus - await workerQueue.enqueue( - "deliverEvent", - { - id: eventLog.id, - }, - { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` } - ); - } + // Create a new event in the database + const eventLog = await tx.eventRecord.create({ + data: { + organizationId: environment.organizationId, + projectId: environment.projectId, + environmentId: environment.id, + eventId: event.id, + name: event.name, + timestamp: event.timestamp ?? new Date(), + payload: event.payload ?? {}, + context: event.context ?? {}, + source: event.source ?? "trigger.dev", + sourceContext, + deliverAt: deliverAt, + externalAccountId: externalAccount ? externalAccount.id : undefined, + }, + }); + + if (this.deliverEvents) { + // Produce a message to the event bus + await workerQueue.enqueue( + "deliverEvent", + { + id: eventLog.id, + }, + { runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` } + ); + } - return eventLog; - }, - { rethrowPrismaErrors: true } - ); + return eventLog; + }); } catch (error) { const prismaError = PrismaErrorSchema.safeParse(error); diff --git a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts index 4369ad8747..3c13f154ac 100644 --- a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts +++ b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts @@ -1,7 +1,9 @@ import { airtable } from "./integrations/airtable"; import { github } from "./integrations/github"; +import { linear } from "./integrations/linear"; import { openai } from "./integrations/openai"; import { plain } from "./integrations/plain"; +import { replicate } from "./integrations/replicate"; import { resend } from "./integrations/resend"; import { sendgrid } from "./integrations/sendgrid"; import { slack } from "./integrations/slack"; @@ -33,8 +35,10 @@ export class IntegrationCatalog { export const integrationCatalog = new IntegrationCatalog({ airtable, github, + linear, openai, plain, + replicate, resend, slack, stripe, diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts new file mode 100644 index 0000000000..bb1266b2bd --- /dev/null +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -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)], + }, + }, + }, +}; diff --git a/apps/webapp/app/services/externalApis/integrations/replicate.ts b/apps/webapp/app/services/externalApis/integrations/replicate.ts new file mode 100644 index 0000000000..74f20cdafd --- /dev/null +++ b/apps/webapp/app/services/externalApis/integrations/replicate.ts @@ -0,0 +1,50 @@ +import type { HelpSample, Integration } from "../types"; + +function usageSample(hasApiKey: boolean): HelpSample { + const apiKeyPropertyName = "apiKey"; + + return { + title: "Using the client", + code: ` +import { Replicate } from "@trigger.dev/replicate"; + +const replicate = new Replicate({ + id: "__SLUG__",${hasApiKey ? `,\n ${apiKeyPropertyName}: process.env.REPLICATE_API_KEY!` : ""} +}); + +client.defineJob({ + id: "replicate-create-prediction", + name: "Replicate - Create Prediction", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.predict", + schema: z.object({ + prompt: z.string(), + version: z.string(), + }), + }), + run: async (payload, io, ctx) => { + return io.replicate.predictions.createAndAwait("await-prediction", { + version: payload.version, + input: { prompt: payload.prompt }, + }); + }, +}); + `, + }; +} + +export const replicate: Integration = { + identifier: "replicate", + name: "Replicate", + packageName: "@trigger.dev/replicate@latest", + authenticationMethods: { + apikey: { + type: "apikey", + help: { + samples: [usageSample(true)], + }, + }, + }, +}; diff --git a/apps/webapp/app/services/jobs/registerJob.server.ts b/apps/webapp/app/services/jobs/registerJob.server.ts index c66a6e1572..401c69c8aa 100644 --- a/apps/webapp/app/services/jobs/registerJob.server.ts +++ b/apps/webapp/app/services/jobs/registerJob.server.ts @@ -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 { + 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; @@ -572,3 +643,7 @@ export class RegisterJobService { }); } } + +function assertExhaustive(x: never): never { + throw new Error("Unexpected object: " + x); +} diff --git a/apps/webapp/app/services/jobs/testJob.server.ts b/apps/webapp/app/services/jobs/testJob.server.ts index 2b154cc0e6..ab95d10856 100644 --- a/apps/webapp/app/services/jobs/testJob.server.ts +++ b/apps/webapp/app/services/jobs/testJob.server.ts @@ -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(), diff --git a/apps/webapp/app/services/runs/continueRun.server.ts b/apps/webapp/app/services/runs/continueRun.server.ts index 00dad4ad22..644cc2e0e9 100644 --- a/apps/webapp/app/services/runs/continueRun.server.ts +++ b/apps/webapp/app/services/runs/continueRun.server.ts @@ -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; diff --git a/apps/webapp/app/services/runs/createRun.server.ts b/apps/webapp/app/services/runs/createRun.server.ts index 34d8cd4e78..a3c7891934 100644 --- a/apps/webapp/app/services/runs/createRun.server.ts +++ b/apps/webapp/app/services/runs/createRun.server.ts @@ -42,29 +42,32 @@ export class CreateRunService { return await $transaction(this.#prismaClient, async (tx) => { // Get the current max number for the given jobId - const currentMaxNumber = await tx.jobRun.aggregate({ + const latestJob = await tx.jobRun.findFirst({ where: { jobId: job.id }, - _max: { number: true }, + orderBy: { id: "desc" }, + select: { + number: true, + }, }); // Increment the number for the new execution - const newNumber = (currentMaxNumber._max.number ?? 0) + 1; + const newNumber = (latestJob?.number ?? 0) + 1; // Create the new execution with the incremented number const run = await tx.jobRun.create({ data: { number: newNumber, preprocess: version.preprocessRuns, - job: { connect: { id: job.id } }, - version: { connect: { id: version.id } }, - event: { connect: { id: eventId } }, - environment: { connect: { id: environment.id } }, - organization: { connect: { id: environment.organizationId } }, - project: { connect: { id: environment.projectId } }, - endpoint: { connect: { id: endpoint.id } }, - queue: { connect: { id: jobQueue.id } }, - externalAccount: eventRecord.externalAccountId - ? { connect: { id: eventRecord.externalAccountId } } + jobId: job.id, + versionId: version.id, + eventId: eventId, + environmentId: environment.id, + organizationId: environment.organizationId, + projectId: environment.projectId, + endpointId: endpoint.id, + queueId: jobQueue.id, + externalAccountId: eventRecord.externalAccountId + ? eventRecord.externalAccountId : undefined, isTest: eventRecord.isTest, }, diff --git a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts index c886427db9..c67a9dd180 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts @@ -1,9 +1,11 @@ import { CachedTaskSchema, RunJobError, + RunJobInvalidPayloadError, RunJobResumeWithTask, RunJobRetryWithTask, RunJobSuccess, + RunJobUnresolvedAuthError, RunSourceContextSchema, } from "@trigger.dev/core"; import type { Task } from "@trigger.dev/database"; @@ -261,6 +263,7 @@ export class PerformRunExecutionV1Service { .flat() .filter(Boolean) .map((t) => CachedTaskSchema.parse(t)), + yieldedExecutions: run.yieldedExecutions, }); if (!response) { @@ -342,6 +345,21 @@ 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; + } + case "YIELD_EXECUTION": { + await this.#resumeYieldedExecution(execution, safeBody.data.key); + + break; + } default: { const _exhaustiveCheck: never = status; throw new Error(`Non-exhaustive match for value: ${status}`); @@ -381,6 +399,40 @@ export class PerformRunExecutionV1Service { }); } + async #resumeYieldedExecution(execution: FoundRunExecution, key: string) { + const { run } = execution; + + return await $transaction(this.#prismaClient, async (tx) => { + await tx.jobRunExecution.update({ + where: { + id: execution.id, + }, + data: { + status: "SUCCESS", + completedAt: new Date(), + run: { + update: { + yieldedExecutions: { + push: key, + }, + }, + }, + }, + }); + + const newJobExecution = await tx.jobRunExecution.create({ + data: { + runId: run.id, + reason: "EXECUTE_JOB", + status: "PENDING", + retryLimit: EXECUTE_JOB_RETRY_LIMIT, + }, + }); + + await enqueueRunExecutionV1(newJobExecution, run.queue.id, run.queue.maxJobs, tx); + }); + } + async #resumeRunWithTask(execution: FoundRunExecution, data: RunJobResumeWithTask) { const { run } = execution; @@ -397,7 +449,9 @@ export class PerformRunExecutionV1Service { // If the task has an operation, then the next performRunExecution will occur // when that operation has finished - if (!data.task.operation) { + // Tasks with callbacks enabled will also get processed separately, i.e. when + // they time out, or on valid requests to their callbackUrl + if (!data.task.operation && !data.task.callbackUrl) { const newJobExecution = await tx.jobRunExecution.create({ data: { runId: run.id, @@ -438,6 +492,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 +629,7 @@ export class PerformRunExecutionV1Service { prisma: PrismaClientOrTransaction, execution: FoundRunExecution, output: Record, - status: "FAILURE" | "ABORTED" = "FAILURE" + status: "FAILURE" | "ABORTED" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE" ): Promise { const { run } = execution; diff --git a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts index 763318153a..f22d13edb1 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts @@ -1,10 +1,17 @@ import { - CachedTask, + API_VERSIONS, + BloomFilter, + ConnectionAuth, + EndpointHeadersSchema, RunJobError, + RunJobInvalidPayloadError, RunJobResumeWithTask, RunJobRetryWithTask, RunJobSuccess, + RunJobUnresolvedAuthError, + RunSourceContext, RunSourceContextSchema, + supportsFeature, } from "@trigger.dev/core"; import { RuntimeEnvironmentType, type Task } from "@trigger.dev/database"; import { generateErrorMessage } from "zod-error"; @@ -16,10 +23,17 @@ import { formatError } from "~/utils/formatErrors.server"; import { safeJsonZodParse } from "~/utils/json"; import { EndpointApi } from "../endpointApi.server"; import { logger } from "../logger.server"; +import { prepareTasksForCaching, prepareTasksForCachingLegacy } from "~/models/task.server"; +import { MAX_RUN_YIELDED_EXECUTIONS } from "~/consts"; +import { ApiEventLog } from "@trigger.dev/core"; +import { RunJobBody } from "@trigger.dev/core"; type FoundRun = NonNullable>>; type FoundTask = FoundRun["tasks"][number]; +// We need to limit the cached tasks to not be too large >3.5MB when serialized +const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000; + export type PerformRunExecutionV2Input = { id: string; reason: "PREPROCESS" | "EXECUTE_JOB"; @@ -151,6 +165,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); @@ -205,38 +242,19 @@ export class PerformRunExecutionV2Service { const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext); - const { response, parser, errorParser, durationInMs } = await client.executeJobRequest({ + const executionBody = await this.#createExecutionBody( + run, + [run.tasks, resumedTask].flat().filter(Boolean), + startedAt, + isRetry, + connections.auth, event, - job: { - id: run.version.job.slug, - version: run.version.version, - }, - run: { - id: run.id, - isTest: run.isTest, - startedAt, - isRetry, - }, - environment: { - id: run.environment.id, - slug: run.environment.slug, - type: run.environment.type, - }, - organization: { - id: run.organization.id, - slug: run.organization.slug, - title: run.organization.title, - }, - account: run.externalAccount - ? { - id: run.externalAccount.identifier, - metadata: run.externalAccount.metadata, - } - : undefined, - connections: connections.auth, - source: sourceContext.success ? sourceContext.data : undefined, - tasks: prepareTasksForRun([run.tasks, resumedTask].flat().filter(Boolean)), - }); + sourceContext.success ? sourceContext.data : undefined + ); + + const { response, parser, errorParser, durationInMs } = await client.executeJobRequest( + executionBody + ); if (!response) { return await this.#failRunExecutionWithRetry({ @@ -244,6 +262,25 @@ export class PerformRunExecutionV2Service { }); } + // Update the endpoint version if it has changed + const rawHeaders = Object.fromEntries(response.headers.entries()); + const headers = EndpointHeadersSchema.safeParse(rawHeaders); + + if ( + headers.success && + headers.data["trigger-version"] && + headers.data["trigger-version"] !== run.endpoint.version + ) { + await this.#prismaClient.endpoint.update({ + where: { + id: run.endpoint.id, + }, + data: { + version: headers.data["trigger-version"], + }, + }); + } + const rawBody = await response.text(); if (!response.ok) { @@ -354,6 +391,20 @@ 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; + } + case "YIELD_EXECUTION": { + await this.#resumeYieldedRun(run, safeBody.data.key, isRetry, durationInMs, executionCount); + break; + } default: { const _exhaustiveCheck: never = status; throw new Error(`Non-exhaustive match for value: ${status}`); @@ -361,6 +412,91 @@ export class PerformRunExecutionV2Service { } } + async #createExecutionBody( + run: FoundRun, + tasks: FoundTask[], + startedAt: Date, + isRetry: boolean, + connections: Record, + event: ApiEventLog, + source?: RunSourceContext + ): Promise { + if (supportsFeature("lazyLoadedCachedTasks", run.endpoint.version)) { + const preparedTasks = prepareTasksForCaching(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT); + + return { + event, + job: { + id: run.version.job.slug, + version: run.version.version, + }, + run: { + id: run.id, + isTest: run.isTest, + startedAt, + isRetry, + }, + environment: { + id: run.environment.id, + slug: run.environment.slug, + type: run.environment.type, + }, + organization: { + id: run.organization.id, + slug: run.organization.slug, + title: run.organization.title, + }, + account: run.externalAccount + ? { + id: run.externalAccount.identifier, + metadata: run.externalAccount.metadata, + } + : undefined, + connections, + source, + tasks: preparedTasks.tasks, + cachedTaskCursor: preparedTasks.cursor, + noopTasksSet: prepareNoOpTasksBloomFilter(tasks), + yieldedExecutions: run.yieldedExecutions, + }; + } + + const preparedTasks = prepareTasksForCachingLegacy(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT); + + return { + event, + job: { + id: run.version.job.slug, + version: run.version.version, + }, + run: { + id: run.id, + isTest: run.isTest, + startedAt, + isRetry, + }, + environment: { + id: run.environment.id, + slug: run.environment.slug, + type: run.environment.type, + }, + organization: { + id: run.organization.id, + slug: run.organization.slug, + title: run.organization.title, + }, + account: run.externalAccount + ? { + id: run.externalAccount.identifier, + metadata: run.externalAccount.metadata, + } + : undefined, + connections, + source, + tasks: preparedTasks.tasks, + }; + } + async #completeRunWithSuccess(run: FoundRun, data: RunJobSuccess, durationInMs: number) { await this.#prismaClient.jobRun.update({ where: { id: run.id }, @@ -394,7 +530,9 @@ export class PerformRunExecutionV2Service { // If the task has an operation, then the next performRunExecution will occur // when that operation has finished - if (!data.task.operation) { + // Tasks with callbacks enabled will also get processed separately, i.e. when + // they time out, or on valid requests to their callbackUrl + if (!data.task.operation && !data.task.callbackUrl) { await enqueueRunExecutionV2(run, tx, { runAt: data.task.delayUntil ?? undefined, resumeTaskId: data.task.id, @@ -432,6 +570,90 @@ 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 #resumeYieldedRun( + run: FoundRun, + key: string, + isRetry: boolean, + durationInMs: number, + executionCount: number + ) { + await $transaction(this.#prismaClient, async (tx) => { + if (run.yieldedExecutions.length + 1 > MAX_RUN_YIELDED_EXECUTIONS) { + return await this.#failRunExecution( + tx, + "EXECUTE_JOB", + run, + { + message: `Run has yielded too many times, the maximum is ${MAX_RUN_YIELDED_EXECUTIONS}`, + }, + "FAILURE", + durationInMs + ); + } + + await tx.jobRun.update({ + where: { + id: run.id, + }, + data: { + executionDuration: { + increment: durationInMs, + }, + executionCount: { + increment: 1, + }, + yieldedExecutions: { + push: key, + }, + }, + select: { + yieldedExecutions: true, + executionCount: true, + }, + }); + + await enqueueRunExecutionV2(run, tx, { + isRetry, + skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + executionCount, + }); + }); + } + async #retryRunWithTask( run: FoundRun, data: RunJobRetryWithTask, @@ -556,7 +778,7 @@ export class PerformRunExecutionV2Service { reason: "EXECUTE_JOB" | "PREPROCESS", run: FoundRun, output: Record, - status: "FAILURE" | "ABORTED" | "TIMED_OUT" = "FAILURE", + status: "FAILURE" | "ABORTED" | "TIMED_OUT" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE", durationInMs: number = 0 ): Promise { await $transaction(prisma, async (tx) => { @@ -617,69 +839,16 @@ export class PerformRunExecutionV2Service { } } -function prepareTasksForRun(possibleTasks: FoundTask[]): CachedTask[] { - const tasks = possibleTasks.filter((task) => task.status === "COMPLETED"); - - // We need to limit the cached tasks to not be too large >3.5MB when serialized - const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000; - - const cachedTasks = new Map(); // Cache for prepared tasks - const cachedTaskSizes = new Map(); // Cache for calculated task sizes - - // Helper function to get the cached prepared task, or prepare and cache if not already cached - function getCachedTask(task: FoundTask): CachedTask { - const taskId = task.id; - if (!cachedTasks.has(taskId)) { - cachedTasks.set(taskId, prepareTaskForRun(task)); - } - return cachedTasks.get(taskId)!; - } - - // Helper function to get the cached task size, or calculate and cache if not already cached - function getCachedTaskSize(task: CachedTask): number { - const taskId = task.id; - if (!cachedTaskSizes.has(taskId)) { - cachedTaskSizes.set(taskId, calculateCachedTaskSize(task)); - } - return cachedTaskSizes.get(taskId)!; - } - - // Prepare tasks and calculate their sizes - const availableTasks = tasks.map((task) => { - const cachedTask = getCachedTask(task); - return { task: cachedTask, size: getCachedTaskSize(cachedTask) }; - }); - - // Sort tasks in ascending order by size - availableTasks.sort((a, b) => a.size - b.size); +function prepareNoOpTasksBloomFilter(possibleTasks: FoundTask[]): string { + const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && task.noop); - // Select tasks using greedy approach - const tasksToRun: CachedTask[] = []; - let remainingSize = TOTAL_CACHED_TASK_BYTE_LIMIT; + const filter = new BloomFilter(BloomFilter.NOOP_TASK_SET_SIZE); - for (const { task, size } of availableTasks) { - if (size <= remainingSize) { - tasksToRun.push(task); - remainingSize -= size; - } + for (const task of tasks) { + filter.add(task.idempotencyKey); } - return tasksToRun; -} - -function prepareTaskForRun(task: FoundTask): CachedTask { - return { - id: task.idempotencyKey, // We should eventually move this back to task.id - status: task.status, - idempotencyKey: task.idempotencyKey, - noop: task.noop, - output: task.output as any, - parentId: task.parentId, - }; -} - -function calculateCachedTaskSize(task: CachedTask): number { - return JSON.stringify(task).length; + return filter.serialize(); } async function findRun(prisma: PrismaClientOrTransaction, id: string) { @@ -714,6 +883,9 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) { output: true, parentId: true, }, + orderBy: { + id: "asc", + }, }, event: true, version: { diff --git a/apps/webapp/app/services/runs/reRun.server.ts b/apps/webapp/app/services/runs/reRun.server.ts index 8c1b8449ed..9a604c16b1 100644 --- a/apps/webapp/app/services/runs/reRun.server.ts +++ b/apps/webapp/app/services/runs/reRun.server.ts @@ -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(), diff --git a/apps/webapp/app/services/runs/startRun.server.ts b/apps/webapp/app/services/runs/startRun.server.ts index 818e310c54..0773eda6cb 100644 --- a/apps/webapp/app/services/runs/startRun.server.ts +++ b/apps/webapp/app/services/runs/startRun.server.ts @@ -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({ diff --git a/apps/webapp/app/services/schedules/registerSchedule.server.ts b/apps/webapp/app/services/schedules/registerSchedule.server.ts index bda3daf894..950942f05b 100644 --- a/apps/webapp/app/services/schedules/registerSchedule.server.ts +++ b/apps/webapp/app/services/schedules/registerSchedule.server.ts @@ -59,6 +59,7 @@ export class RegisterScheduleService { schedule: payload, accountId: payload.accountId, dynamicTrigger, + organizationId: environment.organizationId, }); return registration; diff --git a/apps/webapp/app/services/schedules/registerScheduleSource.server.ts b/apps/webapp/app/services/schedules/registerScheduleSource.server.ts index f27267b855..1aa58fd82d 100644 --- a/apps/webapp/app/services/schedules/registerScheduleSource.server.ts +++ b/apps/webapp/app/services/schedules/registerScheduleSource.server.ts @@ -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; diff --git a/apps/webapp/app/services/sources/registerSourceV1.server.ts b/apps/webapp/app/services/sources/registerSourceV1.server.ts index 2be96c6d62..828d8a0c24 100644 --- a/apps/webapp/app/services/sources/registerSourceV1.server.ts +++ b/apps/webapp/app/services/sources/registerSourceV1.server.ts @@ -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; diff --git a/apps/webapp/app/services/sources/registerSourceV2.server.ts b/apps/webapp/app/services/sources/registerSourceV2.server.ts index 837793080d..4b6bea2527 100644 --- a/apps/webapp/app/services/sources/registerSourceV2.server.ts +++ b/apps/webapp/app/services/sources/registerSourceV2.server.ts @@ -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; diff --git a/apps/webapp/app/services/sources/utils.server.ts b/apps/webapp/app/services/sources/utils.server.ts index 127ca4b7ab..4c2bc7ae7b 100644 --- a/apps/webapp/app/services/sources/utils.server.ts +++ b/apps/webapp/app/services/sources/utils.server.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -export function generateSecret(): string { - return crypto.randomBytes(32).toString("hex"); +export function generateSecret(sizeInBytes = 32): string { + return crypto.randomBytes(sizeInBytes).toString("hex"); } diff --git a/apps/webapp/app/services/tasks/performTaskOperation.server.ts b/apps/webapp/app/services/tasks/performTaskOperation.server.ts index fb5a6304ad..987cb56faa 100644 --- a/apps/webapp/app/services/tasks/performTaskOperation.server.ts +++ b/apps/webapp/app/services/tasks/performTaskOperation.server.ts @@ -1,5 +1,3 @@ -import { env } from "process"; -import { Run } from "~/presenters/RunPresenter.server"; import { FetchOperationSchema, FetchRequestInit, diff --git a/apps/webapp/app/services/tasks/processCallbackTimeout.ts b/apps/webapp/app/services/tasks/processCallbackTimeout.ts new file mode 100644 index 0000000000..948691990d --- /dev/null +++ b/apps/webapp/app/services/tasks/processCallbackTimeout.ts @@ -0,0 +1,76 @@ +import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server"; +import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server"; +import { logger } from "../logger.server"; + +type FoundTask = Awaited>; + +export class ProcessCallbackTimeoutService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(id: string) { + const task = await findTask(this.#prismaClient, id); + + if (!task) { + return; + } + + if (task.status !== "WAITING" || !task.callbackUrl) { + return; + } + + logger.debug("ProcessCallbackTimeoutService.call", { task }); + + return await this.#failTask(task, "Remote callback timeout - no requests received"); + } + + async #failTask(task: NonNullable, error: string) { + await $transaction(this.#prismaClient, async (tx) => { + await tx.taskAttempt.updateMany({ + where: { + taskId: task.id, + status: "PENDING", + }, + data: { + status: "ERRORED", + error + }, + }); + + await tx.task.update({ + where: { id: task.id }, + data: { + status: "ERRORED", + completedAt: new Date(), + output: error, + }, + }); + + await this.#resumeRunExecution(task, tx); + }); + } + + async #resumeRunExecution(task: NonNullable, prisma: PrismaClientOrTransaction) { + await enqueueRunExecutionV2(task.run, prisma, { + skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + }); + } +} + +async function findTask(prisma: PrismaClient, id: string) { + return prisma.task.findUnique({ + where: { id }, + include: { + run: { + include: { + environment: true, + queue: true, + }, + }, + }, + }); +} diff --git a/apps/webapp/app/services/triggers/registerTriggerSourceV2.server.ts b/apps/webapp/app/services/triggers/registerTriggerSourceV2.server.ts index 861d4fb9ad..3118e58a1c 100644 --- a/apps/webapp/app/services/triggers/registerTriggerSourceV2.server.ts +++ b/apps/webapp/app/services/triggers/registerTriggerSourceV2.server.ts @@ -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 { 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 } ); diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 93fc0753af..07493aab51 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -19,6 +19,7 @@ import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent. import { ActivateSourceService } from "./sources/activateSource.server"; import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server"; import { PerformTaskOperationService } from "./tasks/performTaskOperation.server"; +import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout"; import { addMissingVersionField } from "@trigger.dev/core"; const workerCatalog = { @@ -30,6 +31,9 @@ const workerCatalog = { }), scheduleEmail: DeliverEmailSchema, startRun: z.object({ id: z.string() }), + processCallbackTimeout: z.object({ + id: z.string(), + }), performTaskOperation: z.object({ id: z.string(), }), @@ -161,7 +165,8 @@ function getWorkerQueue() { tasks: { "events.invokeDispatcher": { priority: 0, // smaller number = higher priority - maxAttempts: 3, + maxAttempts: 6, + queueName: (payload) => `dispatcher:${payload.id}`, // use a queue for a dispatcher so runs are created sequentially handler: async (payload, job) => { const service = new InvokeDispatcherService(); @@ -239,6 +244,15 @@ function getWorkerQueue() { await service.call(payload.id); }, }, + processCallbackTimeout: { + priority: 0, // smaller number = higher priority + maxAttempts: 3, + handler: async (payload, job) => { + const service = new ProcessCallbackTimeoutService(); + + await service.call(payload.id); + }, + }, performTaskOperation: { priority: 0, // smaller number = higher priority queueName: (payload) => `tasks:${payload.id}`, diff --git a/apps/webapp/app/utils/redactor.ts b/apps/webapp/app/utils/redactor.ts new file mode 100644 index 0000000000..4739590980 --- /dev/null +++ b/apps/webapp/app/utils/redactor.ts @@ -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; + } +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 427e378ce2..5c1be72f7b 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -34,6 +34,7 @@ "@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-json": "^6.0.1", "@codemirror/language": "^6.3.1", + "@codemirror/lint": "^6.4.2", "@codemirror/search": "^6.2.3", "@codemirror/state": "^6.1.3", "@codemirror/view": "^6.5.0", @@ -61,8 +62,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 +74,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", @@ -94,8 +94,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.0", - "react-hotkeys-hook": "^3.4.7", + "react-hotkeys-hook": "^4.4.1", "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", @@ -110,7 +111,7 @@ "tailwindcss-animate": "^1.0.5", "tiny-invariant": "^1.2.0", "ulid": "^2.3.0", - "zod": "3.21.4", + "zod": "3.22.3", "zod-error": "1.5.0" }, "devDependencies": { diff --git a/apps/webapp/prisma/seed.ts b/apps/webapp/prisma/seed.ts index 298c6c4f8c..c8a787a4a8 100644 --- a/apps/webapp/prisma/seed.ts +++ b/apps/webapp/prisma/seed.ts @@ -3,6 +3,7 @@ import { integrationCatalog } from "../app/services/externalApis/integrationCatalog.server"; import { seedCloud } from "./seedCloud"; import { prisma } from "../app/db.server"; +import { createEnvironment } from "~/models/organization.server"; async function seedIntegrationAuthMethods() { for (const [_, integration] of Object.entries(integrationCatalog.getIntegrations())) { @@ -67,12 +68,78 @@ async function seedIntegrationAuthMethods() { } } +async function runDataMigrations() { + await runStagingEnvironmentMigration(); +} + +async function runStagingEnvironmentMigration() { + try { + await prisma.$transaction(async (tx) => { + const existingDataMigration = await tx.dataMigration.findUnique({ + where: { + name: "2023-09-27-AddStagingEnvironments", + }, + }); + + if (existingDataMigration) { + return; + } + + await tx.dataMigration.create({ + data: { + name: "2023-09-27-AddStagingEnvironments", + }, + }); + + console.log("Running data migration 2023-09-27-AddStagingEnvironments"); + + const projectsWithoutStagingEnvironments = await tx.project.findMany({ + where: { + environments: { + none: { + type: "STAGING", + }, + }, + }, + include: { + organization: true, + }, + }); + + for (const project of projectsWithoutStagingEnvironments) { + try { + console.log( + `Creating staging environment for project ${project.slug} on org ${project.organization.slug}` + ); + + await createEnvironment(project.organization, project, "STAGING", undefined, tx); + } catch (error) { + console.error(error); + } + } + + await tx.dataMigration.update({ + where: { + name: "2023-09-27-AddStagingEnvironments", + }, + data: { + completedAt: new Date(), + }, + }); + }); + } catch (error) { + console.error(error); + } +} + async function seed() { await seedIntegrationAuthMethods(); if (process.env.NODE_ENV === "development" && process.env.SEED_CLOUD === "enabled") { await seedCloud(prisma); } + + await runDataMigrations(); } seed() diff --git a/config-packages/tsconfig/integration.json b/config-packages/tsconfig/integration.json new file mode 100644 index 0000000000..753e6091dd --- /dev/null +++ b/config-packages/tsconfig/integration.json @@ -0,0 +1,17 @@ +{ + "extends": "./node18.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "paths": { + "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], + "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], + "@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"], + "@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"] + }, + "declaration": false, + "declarationMap": false, + "baseUrl": ".", + "stripInternal": true + }, + "exclude": ["node_modules"] +} diff --git a/docs/README.md b/docs/README.md index bb304843cb..3a6b828816 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,11 +2,11 @@ ## Install and initial setup -`npm install` +`pnpm install` ## Running the app -`npm run dev` +`pnpm run dev --filter docs` ## View the app locally diff --git a/docs/_snippets/card-react-hooks.mdx b/docs/_snippets/card-react-hooks.mdx new file mode 100644 index 0000000000..bc814e0b99 --- /dev/null +++ b/docs/_snippets/card-react-hooks.mdx @@ -0,0 +1,3 @@ + + Show the live status of Job Runs in your React app + diff --git a/docs/_snippets/frameworks/card-astro.mdx b/docs/_snippets/frameworks/card-astro.mdx new file mode 100644 index 0000000000..6db902f93c --- /dev/null +++ b/docs/_snippets/frameworks/card-astro.mdx @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/astro" +/> diff --git a/docs/_snippets/frameworks/card-express.mdx b/docs/_snippets/frameworks/card-express.mdx new file mode 100644 index 0000000000..3fc9f18c1e --- /dev/null +++ b/docs/_snippets/frameworks/card-express.mdx @@ -0,0 +1,24 @@ + + + + + + + + + + + } + href="/documentation/quickstarts/express" +/> diff --git a/docs/_snippets/frameworks/card-fastify.mdx b/docs/_snippets/frameworks/card-fastify.mdx new file mode 100644 index 0000000000..09ad6d0519 --- /dev/null +++ b/docs/_snippets/frameworks/card-fastify.mdx @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/fastify" +/> diff --git a/docs/_snippets/frameworks/card-nestjs.mdx b/docs/_snippets/frameworks/card-nestjs.mdx new file mode 100644 index 0000000000..84aa845373 --- /dev/null +++ b/docs/_snippets/frameworks/card-nestjs.mdx @@ -0,0 +1,23 @@ + + + + + } + href="/documentation/quickstarts/nestjs" +/> diff --git a/docs/_snippets/frameworks/card-nextjs.mdx b/docs/_snippets/frameworks/card-nextjs.mdx new file mode 100644 index 0000000000..9f6759574d --- /dev/null +++ b/docs/_snippets/frameworks/card-nextjs.mdx @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + +} + href="/documentation/quickstarts/nextjs" + +/> diff --git a/docs/_snippets/frameworks/card-nuxt.mdx b/docs/_snippets/frameworks/card-nuxt.mdx new file mode 100644 index 0000000000..ec3561cb61 --- /dev/null +++ b/docs/_snippets/frameworks/card-nuxt.mdx @@ -0,0 +1,28 @@ + + + + + + + + + + + + } + href="/documentation/quickstarts/nuxt" +/> diff --git a/docs/_snippets/frameworks/card-redwood.mdx b/docs/_snippets/frameworks/card-redwood.mdx new file mode 100644 index 0000000000..f37a1afa2e --- /dev/null +++ b/docs/_snippets/frameworks/card-redwood.mdx @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/redwood" +/> diff --git a/docs/_snippets/frameworks/card-remix.mdx b/docs/_snippets/frameworks/card-remix.mdx new file mode 100644 index 0000000000..9de8732f69 --- /dev/null +++ b/docs/_snippets/frameworks/card-remix.mdx @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/remix" +/> diff --git a/docs/_snippets/frameworks/card-supabase.mdx b/docs/_snippets/frameworks/card-supabase.mdx new file mode 100644 index 0000000000..84afe8c23f --- /dev/null +++ b/docs/_snippets/frameworks/card-supabase.mdx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + href="/documentation/quickstarts/supabase" +/> diff --git a/docs/_snippets/frameworks/card-sveltekit.mdx b/docs/_snippets/frameworks/card-sveltekit.mdx new file mode 100644 index 0000000000..b6f5a768a7 --- /dev/null +++ b/docs/_snippets/frameworks/card-sveltekit.mdx @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + } + href="/documentation/quickstarts/sveltekit" +/> diff --git a/docs/_snippets/how-to-get-run-id.mdx b/docs/_snippets/how-to-get-run-id.mdx new file mode 100644 index 0000000000..8d5d372431 --- /dev/null +++ b/docs/_snippets/how-to-get-run-id.mdx @@ -0,0 +1,5 @@ + + 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. + diff --git a/docs/_snippets/installs/slack.mdx b/docs/_snippets/installs/slack.mdx new file mode 100644 index 0000000000..1c6a647af5 --- /dev/null +++ b/docs/_snippets/installs/slack.mdx @@ -0,0 +1,15 @@ + + +```bash npm +npm install @trigger.dev/slack@latest +``` + +```bash pnpm +pnpm install @trigger.dev/slack@latest +``` + +```bash yarn +yarn add @trigger.dev/slack@latest +``` + + diff --git a/docs/_snippets/jobs/options.mdx b/docs/_snippets/jobs/options.mdx new file mode 100644 index 0000000000..d9fb0cdcb4 --- /dev/null +++ b/docs/_snippets/jobs/options.mdx @@ -0,0 +1,49 @@ + + + + The `id` property is used to uniquely identify the Job. Only change this if you want to create a new Job. + + + The `name` of the Job that you want to appear in the dashboard and logs. You can change this without creating a new Job. + + + 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`. + + + 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. + + + 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. + + + 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: + + + + 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`. + + + 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 + + + + diff --git a/docs/_snippets/manual-setup-astro.mdx b/docs/_snippets/manual-setup-astro.mdx index 55c16726b6..4eb73a95d9 100644 --- a/docs/_snippets/manual-setup-astro.mdx +++ b/docs/_snippets/manual-setup-astro.mdx @@ -5,15 +5,15 @@ To begin, install the necessary packages in your Astro project directory. You ca ```bash npm -npm i @trigger.dev/sdk @trigger.dev/astro +npm i @trigger.dev/sdk@latest @trigger.dev/astro@latest ``` ```bash pnpm -pnpm install @trigger.dev/sdk @trigger.dev/astro +pnpm install @trigger.dev/sdk@latest @trigger.dev/astro@latest ``` ```bash yarn -yarn add @trigger.dev/sdk @trigger.dev/astro +yarn add @trigger.dev/sdk@latest @trigger.dev/astro@latest ``` @@ -32,103 +32,54 @@ 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 `/trigger.ts` or `/src/trigger.ts`, depending on if your project uses a `src` directory, where `` 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 +import { TriggerClient } from "@trigger.dev/sdk"; -2. **Choose Directory:** - - 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 or Otherwise, create it directly in the project root. - -3. **Add Configuration Code:** - - Open the configuration file you created and add the following code: - - ```typescript src/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. **File Location:** - - - You can save the file within the **src** directory or in the project rooot. - - **Example Directory Structure with src:** - - ``` - project-root/ - β”œβ”€β”€ src/ - β”œβ”€β”€ trigger.ts - β”œβ”€β”€ other files... - ``` - - **Example Directory Structure without src:** - - ``` - project-root/ - β”œβ”€β”€ trigger.ts - β”œβ”€β”€ other files... - ``` +export const client = new TriggerClient({ + id: "my-astro-app", + apiKey: import.meta.env.TRIGGER_API_KEY, + apiUrl: import.meta.env.TRIGGER_API_URL, +}); +``` -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-astro-app"** with an appropriate identifier for your project. -## update the astro.config file to enable ssr +## Update the astro.config file to enable SSR (Server Side Rendering) -- You need to enable ssr to use API endpoints that would be in the `pages/api` folder +- 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", }); ``` -## Creating the API Route +To learn more about SSR, head over to the [Astro docs on SSR](https://docs.astro.build/en/guides/server-side-rendering/). -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/js)` within the `pages/api/` directory. -2. Add the following code to `trigger.(ts/js)`: - -```typescript api/trigger.ts/js -import { createAstroRoute } from "@trigger.dev/astro"; -import { client } from "@/trigger"; +## Creating an Example Job -//import your jobs -import "@/jobs"; - -export const { POST } = createAstroRoute(client); -``` - -## Creating the Example Job - -1. Create a folder named `Jobs` alongside your `pages` directory -2. Inside the `Jobs` folder, add two files named `example.(ts/js)` and `index.(ts/js)`. +1. Create a folder named `jobs` alongside your `pages` directory +2. Inside the `jobs` folder, add two files named `example.ts` and `index.ts`. -```typescript example.(ts/js) +```typescript src/jobs/example.ts import { eventTrigger } from "@trigger.dev/sdk"; -import { client } from "@/trigger"; +import { client } from "../trigger"; // your first job client.defineJob({ @@ -148,25 +99,30 @@ client.defineJob({ }); ``` -```typescript index.ts/index.(ts/js) -// import all your job files here - -export * from "./examples"; +```typescript src/jobs/index.ts +// export all your job files here +export * from "./example"; ``` -## Additonal Job Definitions +## Creating the API Route -You can define more job definitions by creating additional files in the `Jobs` folder and exporting them in `index` file. +To establish an API route for interacting with Trigger.dev, follow these steps based on your project's file type and structure -For example, in `index.(ts/js)`, you can export other job files like this: +1. Create a new file named `trigger.ts` within the `pages/api/` directory. +2. Add the following code to `trigger.ts`: -```typescript -// import all your job files here +```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"; -export * from "./examples"; -export * from "./other-job-file"; +//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` @@ -175,7 +131,7 @@ Inside the `package.json` file, add the following configuration under the root o ```json "trigger.dev": { - "endpointId": "my-app" + "endpointId": "my-astro-app" } ``` @@ -189,12 +145,25 @@ Your `package.json` file might look something like this: // ... other dependencies }, "trigger.dev": { - "endpointId": "my-app" + "endpointId": "my-astro-app" } } ``` -Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client. +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 @@ -225,24 +194,27 @@ In a **_separate terminal window or tab_** run: ```bash npm -npx @trigger.dev/cli@latest dev +npx @trigger.dev/cli@latest dev --port 4321 ``` ```bash pnpm -pnpm dlx @trigger.dev/cli@latest dev +pnpm dlx @trigger.dev/cli@latest dev --port 4321 ``` ```bash yarn -yarn dlx @trigger.dev/cli@latest dev +yarn dlx @trigger.dev/cli@latest dev --port 4321 ```
- You can optionally pass the port if you're not running on 3000 by adding - `--port 4321` to the end + Astro by default runs on port 4321. You can optionally pass the hostname if you're not running on localhost by adding `--hostname `. Example, in case your Astro app is running on 0.0.0.0: `--hostname 0.0.0.0`. + +### 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. diff --git a/docs/_snippets/manual-setup-express.mdx b/docs/_snippets/manual-setup-express.mdx index 9ea095ca7f..339fb5cd91 100644 --- a/docs/_snippets/manual-setup-express.mdx +++ b/docs/_snippets/manual-setup-express.mdx @@ -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. + + + +```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 +``` + + + +
+ +Ensure that you execute this command within a Express project. + +## 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 `/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: + + + +```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); +``` + + + +## Creating the Example Job + +Create a Job file. In this case created `/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: + + + +```bash npm +npm run dev +``` + +```bash pnpm +pnpm run dev +``` + +```bash yarn +yarn run dev +``` + + + +You might use `npm run start` instead of dev + +### Run the CLI 'dev' command + +In a **_separate terminal window or tab_** run: + + + +```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 +``` + + +
+ + You can optionally pass the port if you're not running on 3000 by adding + `--port 3001` to the end + + + + You can optionally pass the hostname if you're not running on localhost by adding + `--hostname `. Example, in case your Express is running on 0.0.0.0: `--hostname 0.0.0.0`. + diff --git a/docs/_snippets/manual-setup-fastify.mdx b/docs/_snippets/manual-setup-fastify.mdx index d8711a764c..71e8574cee 100644 --- a/docs/_snippets/manual-setup-fastify.mdx +++ b/docs/_snippets/manual-setup-fastify.mdx @@ -1 +1,328 @@ -We're in the process of building support for the Fastify framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues). +--- +title: "Fastify" +sidebarTitle: "Fastify" +description: "How to manually set up Trigger.dev in your Fastify project" +--- + + +Create a blank project by creating a new Fastify application: + +Step 1: Create a new directory + +Create a new directory for your project and navigate into it: + +```bash +mkdir my-fastify-server +cd my-fastify-server +``` + +Step 2: Initialize your Node.js application + +In your project directory, initialize your Node.js application: + +```bash +npm init -y +``` + +This will create a `package.json` file in your project directory. This file holds various metadata relevant to the project. + +Step 3: Install Fastify + +Now, install Fastify by typing: + +```bash +npm install fastify +``` + +Step 4: Create your server file + +Create a new file in your project directory named `server.js` (or whatever you prefer) and open it in your text editor. +Create a new directory for your project and navigate into it: + +Trigger.dev works with Fastify applications. + +Step 5 (Optional): TypeScript setup + +Install `typescript` and `ts-node` packages + +```bash +npm install typescript ts-node +``` + +Inside the `package.json` file, add the following scripts: + +```json +"scripts": { + "start": "node ./dist/server.js", + "dev": "ts-node ./src/server.ts", + "build": "tsc" +} +``` + +Remember to change the paths in the `start` and `dev` script if you have followed a different folder structure + +Now, create a file named `tsconfig.json` at the root of your project and add the following: + +```json +{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "strict": true, + "allowJs": true, + "module": "commonjs", + "declaration": true, + "target": "ES2019", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": ".", + "strictNullChecks": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": false + }, + "include": ["./src/**/*.ts"] +} +``` + + + +## Installing Required Packages + +To begin, install the necessary packages in your Fastify project directory: + + + +```bash npm + +npm install @trigger.dev/sdk @trigger-dev/fastify + +``` + +```bash pnpm +pnpm install @trigger.dev/sdk @trigger-dev/fastify + +``` + +```bash yarn +yarn add @trigger.dev/sdk @trigger-dev/fastify + +``` + + + +
+ +Ensure that you execute this command within a Fastify project. + +## 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://cloud.trigger.dev + +``` + +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: + +1. **Create Configuration File:** + + In your project directory, create a configuration file named `trigger.ts`. + +2. **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. + +By following these steps, you'll configure the Trigger Client to work with your Fastify project. + +## Creating a Middleware for Trigger.dev + +To establish an API route for interacting with Trigger.dev, follow these steps: + +In `server.(js/ts)` create a middleware for the specific `/api/trigger` route. + +```ts +// In server.(js/ts) +import fastify from "fastify"; +import { createMiddleware } from "@trigger.dev/fastify"; +import { client } from "./trigger"; + +const app = fastify({ + logger: true, +}); + +const middleware = createMiddleware(client); + +app.addHook("preHandler", middleware); + +app.listen({ port: 3000 }, () => { + console.log("Listening on port 3000"); +}); +``` + +Make sure to update the path in the `import { client } from './trigger';` statement to match the location of your Trigger Client configuration file. + +## Creating the Example Job + +1. Create a folder named `Jobs` alongside your Fastify project files. +2. Inside the `Jobs` folder, add two files named `example.js` and `index.js`. + + + +```typescript example.(js/ts) +const { eventTrigger } = require("@trigger.dev/sdk"); +const { client } = require("./trigger"); // Update the path as per your project structure + +// 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 index.ts/index.(ts/js) +// import all your job files here + +module.exports = require("./example"); +``` + + + +## Adding Configuration to `package.json` + +Inside the `package.json` file, add the following configuration under the root object: + +```json +"trigger.dev": { + "endpointId": "my-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-app" + } +} +``` + +Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client. + +## Next Steps + +Start your Fastify project, and your Trigger.dev integration should work seamlessly + +In your project directory, run + + + +```bash npm + +npm run dev + +``` + +```bash pnpm +pnpm run dev + +``` + +```bash yarn +yarn run dev + +``` + + + +This command starts your Fastify server and enables the Trigger.dev integration + +![Your first Job](/images/cli-dev.gif) + + + Ensure your Fastify server is running locally before continuing. You must also leave this server + running while you develop. + + +In a **new terminal window or tab**, you can run Trigger.dev locally: + + + +```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 +``` + + +
+ + You can optionally pass the port if you're not running on port 3000 by adding + `--port 3001` to the end. + + + You can optionally pass the hostname if you're not running on localhost by adding + `--hostname `. Example, in case your Fastify server is running on 0.0.0.0: `--hostname 0.0.0.0`. + + + + If your existing Fastify project utilizes middleware and you encounter any issues, such as + potential conflicts with Trigger.dev, it's recommended to refer to the troubleshooting guide at + [Middleware](/documentation/guides/platforms/Fastify#middleware) for assistance. This guide can + help you address any concerns related to middleware conflicts and ensure the smooth functioning of + your project with Trigger.dev. + diff --git a/docs/_snippets/manual-setup-nestjs.mdx b/docs/_snippets/manual-setup-nestjs.mdx new file mode 100644 index 0000000000..c053c49d48 --- /dev/null +++ b/docs/_snippets/manual-setup-nestjs.mdx @@ -0,0 +1,264 @@ + + Create a blank project by installing the NestJS CLI in your terminal: + +```bash +npm i -g @nestjs/cli +``` + +Then, create an empty project with: + +```bash +nest new project-name +``` + + + +## Installing Required Packages + +To begin, install the necessary packages in your NestJS project directory. You can choose one of the following package managers: + + +```bash npm +npm i @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config +``` + +```bash pnpm +pnpm install @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config +``` + +```bash yarn +yarn add @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config +``` + + + +
+ +Ensure that you execute this command within a NestJS project. + +## 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 line is only necessary if you are self-hosting Trigger +``` + +Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step. + + + This configuration only will be loaded if you use [NestJS + Config](https://docs.nestjs.com/techniques/configuration) or + [dotenv](https://github.com/motdotla/dotenv). + + +## Adding TriggerDev Module + +Open your `app.module.ts`, and add the following inside your `imports`: + +```typescript +import { TriggerDevModule } from "@trigger.dev/nestjs"; +import { Module } from "@nestjs/common"; + +//you need to load the environment variables from .env, this is one way to do it +import "dotenv/config"; + +@Module({ + imports: [ + TriggerDevModule.register({ + id: "my-app", + apiKey: process.env.TRIGGER_API_KEY, + apiUrl: process.env.TRIGGER_API_URL, + }), + // if you use NestJS Config, you can do like this: + // TriggerDevModule.registerAsync({ + // useFactory: (configService: ConfigService) => ({ + // id: 'my-app', + // apiKey: configService.get("TRIGGER_API_KEY"), + // apiUrl: configService.get("TRIGGER_API_URL"), + // }), + // inject: [ConfigService], + // }), + ], +}) +export class AppModule { + //... +} +``` + +Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier. + +By following these steps, you'll configure the Trigger Client to work with your project. + +## Creating the Example Job + +When you add `TriggerDevModule` to your project, you will can have access to the `TriggerClient` instance by using the `@InjectTriggerDevClient()` decorator in the constructor. + +Now, let's create an example job to test the integration. + +1. Create a controller named `job.controller.ts` alongside your `app.module.ts` +2. Inside that controller, add the following code: + + + +```typescript job.controller.ts +import { Controller, Get } from "@nestjs/common"; +import { InjectTriggerDevClient } from "@trigger.dev/nestjs"; +import { eventTrigger, TriggerClient } from "@trigger.dev/sdk"; + +@Controller() +export class JobController { + constructor(@InjectTriggerDevClient() private readonly client: TriggerClient) { + this.client.defineJob({ + id: "test-job", + name: "Test Job One", + version: "0.0.1", + trigger: eventTrigger({ + name: "test.event", + }), + run: async (payload, io, ctx) => { + await io.logger.info("Hello world!", { payload }); + + return { + message: "Hello world!", + }; + }, + }); + } + + @Get() + getHello(): string { + return `Running Trigger.dev with client-id ${this.client.id}`; + } +} +``` + +Now, add this controller to your `app.module.ts`: + +```typescript app.module.ts +import { TriggerDevModule } from "@trigger.dev/nestjs"; +import { Module } from "@nestjs/common"; +import { JobController } from "./job.controller"; + +//you need to load the environment variables from .env, this is one way to do it +import "dotenv/config"; + +@Module({ + controllers: [JobController], + imports: [ + TriggerDevModule.register({ + id: "my-app", + apiKey: process.env.TRIGGER_API_KEY, + apiUrl: process.env.TRIGGER_API_URL, + }), + // if you use NestJS Config, you can do like this: + // TriggerDevModule.registerAsync({ + // useFactory: (configService: ConfigService) => ({ + // id: 'my-app', + // apiKey: configService.get("TRIGGER_API_KEY"), + // apiUrl: configService.get("TRIGGER_API_URL"), + // }), + // inject: [ConfigService], + // }), + ], +}) +export class AppModule { + //... +} +``` + + + +
+ + You can import the Trigger.dev client inside any `service` or `controller`, we recommend you to + create specialized `service` for each job you have for a better maintainability. + + +## Adding Configuration to `package.json` + +Inside the `package.json` file, add the following configuration under the root object: + +```json +"trigger.dev": { + "endpointId": "my-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-app" + } +} +``` + +Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client. + +## Running + +### Run your NestJS app + +Run your NestJS app locally, like you normally would. For example: + + + +```bash npm +npm run start +``` + +```bash pnpm +pnpm run start +``` + +```bash yarn +yarn run start +``` + + + +### Run the CLI 'dev' command + +In a **_separate terminal window or tab_** run: + + + +```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 +``` + + +
+ + You can optionally pass the port if you're not running on 3000 by adding + `--port 3001` to the end + + + + You can optionally pass the hostname if you're not running on localhost by adding + `--hostname `. Example, in case your Remix is running on 0.0.0.0: `--hostname 0.0.0.0`. + diff --git a/docs/_snippets/manual-setup-remix.mdx b/docs/_snippets/manual-setup-remix.mdx index cb89f338f6..d6f7d20e52 100644 --- a/docs/_snippets/manual-setup-remix.mdx +++ b/docs/_snippets/manual-setup-remix.mdx @@ -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 `/app/trigger.ts`, where `` 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. - -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 :** +export const client = new TriggerClient({ + id: "my-app", + apiKey: process.env.TRIGGER_API_KEY, + apiUrl: process.env.TRIGGER_API_URL, +}); +``` - ``` - project-root/ - β”œβ”€β”€ app/ - β”œβ”€β”€ routes/ - β”œβ”€β”€ trigger.ts - β”œβ”€β”€ other files... - ``` +Replace **"my-app"** with an appropriate identifier for your project. ## Creating the API Route diff --git a/docs/_snippets/quickstart-cli-dev.mdx b/docs/_snippets/quickstart-cli-dev.mdx new file mode 100644 index 0000000000..866a8ee4a9 --- /dev/null +++ b/docs/_snippets/quickstart-cli-dev.mdx @@ -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: + + + +```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 +``` + + +
+ + You can optionally pass the port if you're not running on the default port by adding + `--port 3001` to the end. + diff --git a/docs/_snippets/quickstart-example-job.mdx b/docs/_snippets/quickstart-example-job.mdx new file mode 100644 index 0000000000..6bf8b662f0 --- /dev/null +++ b/docs/_snippets/quickstart-example-job.mdx @@ -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 + }, +}); +``` diff --git a/docs/_snippets/quickstart-running-your-job.mdx b/docs/_snippets/quickstart-running-your-job.mdx new file mode 100644 index 0000000000..bd18bb531f --- /dev/null +++ b/docs/_snippets/quickstart-running-your-job.mdx @@ -0,0 +1,18 @@ + + +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!** + + diff --git a/docs/_snippets/quickstart-setup-steps.mdx b/docs/_snippets/quickstart-setup-steps.mdx new file mode 100644 index 0000000000..fbcf9e9c32 --- /dev/null +++ b/docs/_snippets/quickstart-setup-steps.mdx @@ -0,0 +1,80 @@ + + +You can either: + +- Use the [Trigger.dev Cloud](https://cloud.trigger.dev). +- Or [self-host](/documentation/guides/self-hosting) the service. + + + + + +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. + + + + + +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) + + + + + +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: + + + +```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 +``` + + + +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. + + + + + +Make sure your site is running locally, we will connect to it to register your Jobs. + +You must leave this running for the rest of the steps. + + + +```bash npm +npm run dev +``` + +```bash pnpm +pnpm run dev +``` + +```bash yarn +yarn run dev +``` + + + + diff --git a/docs/_snippets/quickstart-whats-next.mdx b/docs/_snippets/quickstart-whats-next.mdx new file mode 100644 index 0000000000..5d815504e5 --- /dev/null +++ b/docs/_snippets/quickstart-whats-next.mdx @@ -0,0 +1,20 @@ +## What's next? + + + + A Guide for how to create your first real Job + + + Learn more about how Trigger.dev works and how it can help you. + + + One of the quickest ways to learn how Trigger.dev works is to view some example Jobs. + + + Struggling getting setup or have a question? We're here to help. + + diff --git a/docs/_snippets/react-hook-types.mdx b/docs/_snippets/react-hook-types.mdx new file mode 100644 index 0000000000..03608f73d1 --- /dev/null +++ b/docs/_snippets/react-hook-types.mdx @@ -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. diff --git a/docs/documentation/concepts/client-adaptors.mdx b/docs/documentation/concepts/client-adaptors.mdx index 98fc4dc56e..1cb5af7acf 100644 --- a/docs/documentation/concepts/client-adaptors.mdx +++ b/docs/documentation/concepts/client-adaptors.mdx @@ -24,10 +24,12 @@ Adaptors allows Clients to receive data from the Trigger API. They do this by cr Each platform has one or more adaptors, see the guides below: -| Platform | Adaptor | -| ------------------------------------------------- | -------------------- | -| [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 | +| Platform | Adaptor | +| -------------------------------------------------- | -------------------- | +| [Next.js](/documentation/guides/platforms/nextjs) | `createPagesRoute()` | +| [Next.js](/documentation/guides/platforms/nextjs) | `createAppRoute()` | +| [NestJS](/documentation/guides/manual/nestjs) | `TriggerDevModule` | +| [Astro](/documentation/guides/platforms/astro) | `createAstroRoute()` | +| [Remix](/documentation/guides/platforms/remix) | `createRemixRoute()` | +| [Fastify](/documentation/guides/platforms/fastify) | `createMiddleware()` | +| Express | Coming soon | diff --git a/docs/documentation/concepts/environments-endpoints.mdx b/docs/documentation/concepts/environments-endpoints.mdx index 689cf7cdd9..09239616d1 100644 --- a/docs/documentation/concepts/environments-endpoints.mdx +++ b/docs/documentation/concepts/environments-endpoints.mdx @@ -27,6 +27,10 @@ The `DEV` environment should only be used for local development. It's where you +### Staging + +The `STAGING` environment is useful for testing your Jobs against your staging server, if you have one. STAGING works identically to PROD. + ### Production The `PROD` environment is where your Jobs will run in production. It's where you can run your Jobs against real data. diff --git a/docs/documentation/concepts/limitations.mdx b/docs/documentation/concepts/limitations.mdx index 4f1330d870..ab9b80a8c6 100644 --- a/docs/documentation/concepts/limitations.mdx +++ b/docs/documentation/concepts/limitations.mdx @@ -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. diff --git a/docs/documentation/concepts/triggers/dynamic.mdx b/docs/documentation/concepts/triggers/dynamic.mdx index dda10dfede..5a12b2b5bc 100644 --- a/docs/documentation/concepts/triggers/dynamic.mdx +++ b/docs/documentation/concepts/triggers/dynamic.mdx @@ -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, + }); }, }); ``` diff --git a/docs/documentation/concepts/triggers/introduction.mdx b/docs/documentation/concepts/triggers/introduction.mdx index c626e7852b..c4a56cee34 100644 --- a/docs/documentation/concepts/triggers/introduction.mdx +++ b/docs/documentation/concepts/triggers/introduction.mdx @@ -1,30 +1,19 @@ --- -title: Introduction +title: "Triggers: Introduction" +sidebarTitle: "Introduction" description: "A Trigger is what starts a Job Run. It can be a webhook, a schedule, or an event." --- We currently support three types of Triggers: Webhooks, Scheduled, and Events. You can use any of these to start a Job Run. - + Start your Jobs in realtime when events happen in APIs - + Run a Job on a repeating schedule - + Run your Job when you send events with data + This only needs to be done once for each environment @@ -32,11 +29,7 @@ There are two ways to do this: > Manually refresh in your Trigger.dev dashboard - + Automatically refresh by using our webhook diff --git a/docs/documentation/guides/manual/nestjs.mdx b/docs/documentation/guides/manual/nestjs.mdx new file mode 100644 index 0000000000..e29c1ae272 --- /dev/null +++ b/docs/documentation/guides/manual/nestjs.mdx @@ -0,0 +1,7 @@ +--- +title: "NestJS" +sidebarTitle: "NestJS" +description: "How to manually setup Trigger.dev in your NestJS project" +--- + + diff --git a/docs/documentation/guides/manual/nextjs.mdx b/docs/documentation/guides/manual/nextjs.mdx index 56285d2f8e..6d36084cfd 100644 --- a/docs/documentation/guides/manual/nextjs.mdx +++ b/docs/documentation/guides/manual/nextjs.mdx @@ -22,19 +22,15 @@ To begin, install the necessary packages in your Next.js project directory. You ```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 ``` @@ -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: - -1. **Create Configuration File:** - - 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`). - -2. **Choose Directory:** - - 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. - -3. **Add Configuration Code:** +Create a file at `/src/trigger.ts` or `/trigger.ts` depending on whether you're using the `src` directory or not. `` represents the root directory of your project. - Open the configuration file you created and add the following code: +Next, add the following code to the file which creates and exports a new `TriggerClient`: - ```typescript - // trigger.ts (for TypeScript) or trigger.js (for JavaScript) +```typescript src/trigger.(ts/js) +// trigger.ts (for TypeScript) or trigger.js (for JavaScript) - import { TriggerClient } from "@trigger.dev/sdk"; +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... - ``` +export const client = new TriggerClient({ + id: "my-app", + apiKey: process.env.TRIGGER_API_KEY, + apiUrl: process.env.TRIGGER_API_URL, +}); +``` -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: - - Make sure your Next.js site is running locally before continuing. You must also leave this `dev` - terminal command running while you develop. - + -In a **new terminal window or tab** run: +```bash npm +npm run dev +``` + +```bash pnpm +pnpm run dev +``` + +```bash yarn +yarn run dev +``` + + + +### Run the CLI 'dev' command + +In a **_separate terminal window or tab_** run: @@ -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 + You can optionally pass the hostname if you're not running on localhost by adding - `--hostname `. Example, in case your Next.js is running on 0.0.0.0: `--hostname 0.0.0.0`. + `--hostname `. Example, in case your Remix is running on 0.0.0.0: `--hostname 0.0.0.0`. diff --git a/docs/documentation/guides/react-hooks-automatic.mdx b/docs/documentation/guides/react-hooks-automatic.mdx new file mode 100644 index 0000000000..d097442556 --- /dev/null +++ b/docs/documentation/guides/react-hooks-automatic.mdx @@ -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. + + + +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
Error: {error.message}
; + } + + if (!data) { + return
Loading...
; + } + + return ( +
+

{data.name}

+

Runs: {data.runs?.length}

+
+ {data.runs?.map((run) => ( +
+

+ Run {run.id}: {run.status} +

+
+ ))} +
+
+ ); +} +``` + +#### useRunDetails + +The `useRunDetails` hook will get the details of a specific Run. You can use this to show the status of a specific Run. + + + +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
Error: {error.message}
; + } + + if (!data) { + return
Loading...
; + } + + return ( +
+

Run {data.id}

+

Status: {data.status}

+
+ {data.tasks?.map((task) => ( +
+

+ Task {task.id}: {task.status} +

+
+ ))} +
+
+ ); +} +``` + +#### 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. + + + +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
Error: {error.message}
; + } + + if (!data) { + return
Loading...
; + } + + return ( +
+

Run {data.id}

+

Status: {data.status}

+
+ {data.tasks?.map((task) => ( +
+

+ Task {task.id}: {task.status} +

+
+ ))} +
+
+ ); +} +``` diff --git a/docs/documentation/guides/react-hooks-statuses.mdx b/docs/documentation/guides/react-hooks-statuses.mdx new file mode 100644 index 0000000000..eb0bb08f34 --- /dev/null +++ b/docs/documentation/guides/react-hooks-statuses.mdx @@ -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. + + + +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

Loading...

; + } + + if (fetchStatus === "error") { + return ( +
+

{error.name}

+

{error.message}

+
+ ); + } + + return ( + <> + //you receive the overall status of the run, e.g. SUCCESS, FAIL +
Run status: {run.status}
+
+ {statuses.map((status) => { + switch (status.key) { + case "generating-memes": { + const urls = status.data?.urls as string[] | undefined; + return ( +
+ // Will display: "Generating memes: loading" +

+ {status.label}: {status.state} +

+ //will render the memes as images + {urls?.map((url) => )} +
+ ); + } + } + })} +
+ //this is what's returned from the run function + {run.output && ( + +
{JSON.stringify(run.output, null, 2)}
+
+ )} + + ); +} +``` + +## useRunStatuses + +The `useRunStatuses` hook will give you the statuses and overview data of a specific Run. + + + +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

Loading...

; + } + + if (fetchStatus === "error") { + return ( +
+

{error.name}

+

{error.message}

+
+ ); + } + + return ( + <> + //you receive the overall status of the run, e.g. SUCCESS, FAIL +
Run status: {run.status}
+
+ {statuses.map((status) => { + switch (status.key) { + case "generating-memes": { + const urls = status.data?.urls as string[] | undefined; + return ( +
+ // Will display: "Generating memes: loading" +

+ {status.label}: {status.state} +

+ //will render the memes as images + {urls?.map((url) => )} +
+ ); + } + } + })} +
+ //this is what's returned from the run function + {run.output && ( + +
{JSON.stringify(run.output, null, 2)}
+
+ )} + + ); +} +``` diff --git a/docs/documentation/guides/react-hooks.mdx b/docs/documentation/guides/react-hooks.mdx index 5df366aa87..0132daae72 100644 --- a/docs/documentation/guides/react-hooks.mdx +++ b/docs/documentation/guides/react-hooks.mdx @@ -1,5 +1,6 @@ --- -title: "React hooks" +title: "React hooks: Overview" +sidebarTitle: "Overview" description: "How to show the live status of Job Runs in your React app" --- @@ -9,207 +10,168 @@ You can display the live progress of a Job Run to your users, including the stat -## 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 + + + Add the `@trigger.dev/react` package to your project: -Add the `@trigger.dev/react` package to your project: + - + ```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 -``` + - + + + 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. + + + 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. + + + + + + + Add the `NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API. + + ```sh .env.local + #... + TRIGGER_API_KEY=[your_private_api_key] + NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=[your_public_api_key] + #... + ``` + + Your private API key should already be in there. -![Get your public API Key](/images/environments-public-apikey.png) - -You should copy the `PUBLIC` API key for the dev environment. - - - 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. - - -### 3. 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. - -```sh .env.local -#... -TRIGGER_API_KEY=[your_private_api_key] -NEXT_PUBLIC_TRIGGER_API_KEY=[your_public_api_key] -#... -``` - -Your private API key should already be in there. - -### 4. 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. - -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. - -```tsx app/layout.tsx -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - - {children} - - - - ); -} -``` - -### 5. Add hooks to your components - -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. - - - -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
Error: {error.message}
; - } - - if (!data) { - return
Loading...
; - } - - return ( -
-

{data.name}

-

Runs: {data.runs?.length}

-
- {data.runs?.map((run) => ( -
-

- Run {run.id}: {run.status} -

-
- ))} -
-
- ); -} -``` - -#### 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. - - - 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. - - -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
Error: {error.message}
; - } - - if (!data) { - return
Loading...
; - } - - return ( -
-

Run {data.id}

-

Status: {data.status}

-
- {data.tasks?.map((task) => ( -
-

- Task {task.id}: {task.status} -

-
- ))} -
-
- ); -} -``` - -#### 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. - - - -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
Error: {error.message}
; - } - - if (!data) { - return
Loading...
; - } - - return ( -
-

Run {data.id}

-

Status: {data.status}

-
- {data.tasks?.map((task) => ( -
-

- Task {task.id}: {task.status} -

-
- ))} -
-
- ); -} -``` + `NEXT_PUBLIC_` is a special prefix that exposes the environment variable to your users' web browsers. +
+ + + Add the `TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API. + + ```sh .env + #... + TRIGGER_API_KEY=[your_private_api_key] + TRIGGER_PUBLIC_API_KEY=[your_public_api_key] + #... + ``` + + You will need to pass this value from the server to the client. We recommend you do this in your Root loader. + + + +
+ +
+ + + + The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components. + + 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. + + + + + ```tsx app/layout.tsx + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); + } + ``` + + + + + ```tsx app/root.tsx + //return the public key env var from the loader so it's available in the browser + export const loader = async ({ request }: LoaderArgs) => { + //...other code + + const triggerPublicApiKey = env.TRIGGER_PUBLIC_API_KEY!; + + return json({ + //...other data + triggerPublicApiKey + }); + } + + //Your default export, i.e. the page component + export default function App() { + const { + //...other data + triggerPublicApiKey + } = useLoaderData(); + + return ( + + + + + + + {/* wrap your outlet in this */} + + + + + + + + + + ); + } + ``` + + + + + +
+ +## Two ways to report Run progress + +**Automatic progress** – without writing additional code in your Job you can get updates on the overall run status and individual tasks inside the run. + +**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. + + + + Receive coarse updates without writing additional Job code + + + Add statuses to your Job code for fine-grained UI updates + + diff --git a/docs/documentation/guides/testing-jobs.mdx b/docs/documentation/guides/testing-jobs.mdx index 04ce3bf51c..d988ba20ff 100644 --- a/docs/documentation/guides/testing-jobs.mdx +++ b/docs/documentation/guides/testing-jobs.mdx @@ -14,9 +14,32 @@ There's a tab on the Job page called **Test**. Or you can click the "Test" butto ![Your options on the Test page](/images/test-annotated.png) -1. Select the environment you'd like the test to run against. -2. Some Triggers provide example payloads that you can select from. This will populate the code editor below. -3. When you're happy with the payload, click **Run test**. + + + You will see errors inline if you have any syntax errors. You can use the *Clear* and *Copy* + buttons in the corner. + + + Some Triggers provide example payloads that you can select from. When selected they will + populate the code editor below. + + + If you have previously done Runs, you can select from the most recent payloads. When selected + they will populate the code editor below. + + + If this Job has associated Accounts, enter an Account ID. See [testing with account + ids](/documentation/guides/using-integrations-byo-auth#testing-jobs-with-account-id) for more + information. + + + Select the environment you'd like the test to run against. + + + When you're happy with the payload, click **Run test**. Or press the shortcut key: `Cmd + Enter` + on Mac, `Ctrl + Enter` on Windows. + + ## Identifying test runs diff --git a/docs/documentation/guides/using-integrations-apikeys.mdx b/docs/documentation/guides/using-integrations-apikeys.mdx index 330aee59b3..512dcc9e83 100644 --- a/docs/documentation/guides/using-integrations-apikeys.mdx +++ b/docs/documentation/guides/using-integrations-apikeys.mdx @@ -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 diff --git a/docs/documentation/guides/using-integrations-byo-auth.mdx b/docs/documentation/guides/using-integrations-byo-auth.mdx new file mode 100644 index 0000000000..a46d94680b --- /dev/null +++ b/docs/documentation/guides/using-integrations-byo-auth.mdx @@ -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) + + + 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. + + +## 1. Install the Slack integration package + + + +## 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 + + + +```bash npm +npm install @clerk/backend@latest +``` + +```bash pnpm +pnpm install @clerk/backend@latest +``` + +```bash yarn +yarn add @clerk/backend@latest +``` + + + +## 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). + + + 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. + + +## 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) + + + 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, +}); +``` + + + +## 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: + + + + +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, +}); +``` + + + + + +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! πŸ₯‡", + }); + }, +}); +``` + + + + + +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, + }; +}); +``` + + + 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`. + + + + + + + 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, + } + ); +} +``` + + + + +# 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; + +type AuthResolverResult = { + type: "apiKey" | "oauth"; + token: string; + additionalFields?: Record; +}; +``` + +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", + }, + }; +}); +``` diff --git a/docs/documentation/guides/using-integrations.mdx b/docs/documentation/guides/using-integrations.mdx index b8af49a61f..45833102e1 100644 --- a/docs/documentation/guides/using-integrations.mdx +++ b/docs/documentation/guides/using-integrations.mdx @@ -1,12 +1,12 @@ --- -title: "Using Integrations" -description: "How to use Integrations" +title: "Integrations: Overview" +sidebarTitle: "Overview" +description: "How to use Trigger.dev Integrations" --- - 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. [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
+ + Use our integrations with your user’s auth credentials, using Clerk.com, Nango.dev, or rolling + your own with our custom auth resolvers + ## 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. - 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. diff --git a/docs/documentation/introduction.mdx b/docs/documentation/introduction.mdx index 0a962b2478..a9f68d1bf8 100644 --- a/docs/documentation/introduction.mdx +++ b/docs/documentation/introduction.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Getting Started: Introduction" +sidebarTitle: "Introduction" description: "Welcome to the Trigger.dev documentation." --- @@ -59,7 +60,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.
- + 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. diff --git a/docs/documentation/quickstarts/astro.mdx b/docs/documentation/quickstarts/astro.mdx index f9b9968caf..f1d0a92982 100644 --- a/docs/documentation/quickstarts/astro.mdx +++ b/docs/documentation/quickstarts/astro.mdx @@ -4,4 +4,81 @@ sidebarTitle: "Astro" description: "Start creating Jobs in 5 minutes in your Astro project." --- - +This quick start guide will get you up and running with Trigger.dev. + + +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 +``` + + + + + + + + + + + + +You can modify your `package.json` to run both the Astro server and the CLI `dev` command together. + +1. Install the `concurrently` package: + + + +```bash npm +npm install concurrently --save-dev +``` + +```bash pnpm +pnpm install concurrently --save-dev +``` + +```bash yarn +yarn add concurrently --dev +``` + + + +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", + //... +} +//... +``` + + + + + + + + +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: + + + +If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section: + +![Your first Job](/images/first-job.png) + + + + + + + + diff --git a/docs/documentation/quickstarts/introduction.mdx b/docs/documentation/quickstarts/introduction.mdx index 48891d3989..e5e31232fb 100644 --- a/docs/documentation/quickstarts/introduction.mdx +++ b/docs/documentation/quickstarts/introduction.mdx @@ -1,5 +1,5 @@ --- -title: "Introduction" +title: "Quick Starts: Introduction" sidebarTitle: "Introduction" --- @@ -8,261 +8,19 @@ sidebarTitle: "Introduction" ## Select a framework to get started… - - - - - - - - - - - - - - - - -} - href="/documentation/quickstarts/nextjs" - -> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/remix"> - - - - - - - - - - - -} href="/documentation/quickstarts/express"> - - - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/redwood"> - - - - - - - - - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/astro"> - - - - - - - - - - - - -} href="/documentation/quickstarts/nuxt"> - - - - - - - - - - - - - - -} href="/documentation/quickstarts/sveltkit"> - - - - - - - - - - - - - - - - - - -} href="/documentation/quickstarts/fastify"/> - + + + + + + + + + ## Or quickly setup Trigger.dev with… - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - href="/documentation/quickstarts/supabase" - -> - + diff --git a/docs/documentation/quickstarts/nestjs.mdx b/docs/documentation/quickstarts/nestjs.mdx new file mode 100644 index 0000000000..46bc1b03b6 --- /dev/null +++ b/docs/documentation/quickstarts/nestjs.mdx @@ -0,0 +1,7 @@ +--- +title: "NestJS Quick Start" +sidebarTitle: "NestJS" +description: "Start creating Jobs in 5 minutes in your NestJS project." +--- + + diff --git a/docs/documentation/quickstarts/nextjs.mdx b/docs/documentation/quickstarts/nextjs.mdx index 2cefe44626..4e59b46247 100644 --- a/docs/documentation/quickstarts/nextjs.mdx +++ b/docs/documentation/quickstarts/nextjs.mdx @@ -17,106 +17,12 @@ Trigger.dev works with either the Pages or App Router configuration. -## 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. - -### 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: - - - -```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 -``` - - - -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. - -You must leave this running for the rest of the steps. - - - -```bash npm -npm run dev -``` - -```bash pnpm -pnpm run dev -``` - -```bash yarn -yarn run dev -``` - - - -## 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: - - - -```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 -``` - - -
- - You can optionally pass the port if you're not running on 3000 by adding - `--port 3001` to the end - + @@ -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,27 @@ yarn add concurrently --dev -## Your first job +
+ + -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`. +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!", - }; - }, -}); -``` + 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 - -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! + -## What's next? +
+ - - A Guide for how to create your first real Job - - - Learn more about how Trigger.dev works and how it can help you. - - - One of the quickest ways to learn how Trigger.dev works is to view some example Jobs. - - - Struggling getting setup or have a question? We're here to help. - + diff --git a/docs/documentation/quickstarts/remix.mdx b/docs/documentation/quickstarts/remix.mdx index a3a2bb174f..dcdf7f6c59 100644 --- a/docs/documentation/quickstarts/remix.mdx +++ b/docs/documentation/quickstarts/remix.mdx @@ -4,4 +4,86 @@ sidebarTitle: "Remix" description: "Start creating Jobs in 5 minutes in your Remix project." --- - +This quick start guide will get you up and running with Trigger.dev. + + +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. + + + + + + + + + + + + +You can modify your `package.json` to run both the Remix server and the CLI `dev` command together. + +1. Install the `concurrently` package: + + + +```bash npm +npm install concurrently --save-dev +``` + +```bash pnpm +pnpm install concurrently --save-dev +``` + +```bash yarn +yarn add concurrently --dev +``` + + + +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", + //... +} +//... +``` + + + + + + + + +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: + + + +If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section: + +![Your first Job](/images/first-job.png) + + + + + + + + + + + diff --git a/docs/documentation/quickstarts/supabase.mdx b/docs/documentation/quickstarts/supabase.mdx index 63a27093aa..8690c70175 100644 --- a/docs/documentation/quickstarts/supabase.mdx +++ b/docs/documentation/quickstarts/supabase.mdx @@ -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? diff --git a/docs/examples/introduction.mdx b/docs/examples/introduction.mdx index d17bb6d395..f5804133e4 100644 --- a/docs/examples/introduction.mdx +++ b/docs/examples/introduction.mdx @@ -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 | πŸ› οΈ | diff --git a/docs/images/byo-auth/dynamic-schedule-task.png b/docs/images/byo-auth/dynamic-schedule-task.png new file mode 100644 index 0000000000..6fbbffdce2 Binary files /dev/null and b/docs/images/byo-auth/dynamic-schedule-task.png differ diff --git a/docs/images/byo-auth/run-dashboard-account-id.png b/docs/images/byo-auth/run-dashboard-account-id.png new file mode 100644 index 0000000000..831fa882b2 Binary files /dev/null and b/docs/images/byo-auth/run-dashboard-account-id.png differ diff --git a/docs/images/byo-auth/run-test-account-id.png b/docs/images/byo-auth/run-test-account-id.png new file mode 100644 index 0000000000..8120e90746 Binary files /dev/null and b/docs/images/byo-auth/run-test-account-id.png differ diff --git a/docs/images/test-annotated.png b/docs/images/test-annotated.png index a224e3fe49..d34b50d33d 100644 Binary files a/docs/images/test-annotated.png and b/docs/images/test-annotated.png differ diff --git a/docs/integrations/apis/github.mdx b/docs/integrations/apis/github.mdx index c99e71dc0f..c001d7da19 100644 --- a/docs/integrations/apis/github.mdx +++ b/docs/integrations/apis/github.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "GitHub: Introduction" +sidebarTitle: "Introduction" --- diff --git a/docs/integrations/apis/linear.mdx b/docs/integrations/apis/linear.mdx new file mode 100644 index 0000000000..c1b5287d66 --- /dev/null +++ b/docs/integrations/apis/linear.mdx @@ -0,0 +1,465 @@ +--- +title: Linear +description: "Streamline your project and issue tracking" +--- + + + +## 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: + + + +```bash npm +npm install @trigger.dev/linear@latest +``` + +```bash pnpm +pnpm add @trigger.dev/linear@latest +``` + +```bash yarn +yarn add @trigger.dev/linear@latest +``` + + + +## 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. | diff --git a/docs/integrations/apis/replicate.mdx b/docs/integrations/apis/replicate.mdx new file mode 100644 index 0000000000..978bb51c19 --- /dev/null +++ b/docs/integrations/apis/replicate.mdx @@ -0,0 +1,170 @@ +--- +title: Replicate +description: "Run machine learning tasks easily at scale" +--- + + + +## Installation + +To get started with the Replicate integration on Trigger.dev, you need to install the `@trigger.dev/replicate` package. +You can do this using npm, pnpm, or yarn: + + + +```bash npm +npm install @trigger.dev/replicate@latest +``` + +```bash pnpm +pnpm add @trigger.dev/replicate@latest +``` + +```bash yarn +yarn add @trigger.dev/replicate@latest +``` + + + +## Authentication + +To use the Replicate API with Trigger.dev, you have to provide an API Key. + +### API Key + +You can create an API Key in your [Account Settings](https://replicate.com/account/api-tokens). + +```ts +import { Replicate } from "@trigger.dev/replicate"; + +//this will use the passed in API key (defined in your environment variables) +const replicate = new Replicate({ + id: "replicate", + apiKey: process.env["REPLICATE_API_KEY"], +}); +``` + +## Usage + +Include the Replicate integration in your Trigger.dev job. + +```ts +client.defineJob({ + id: "replicate-cinematic-prompt", + name: "Replicate - Cinematic Prompt", + version: "0.1.0", + integrations: { replicate }, + trigger: eventTrigger({ + name: "replicate.cinematic", + schema: z.object({ + prompt: z.string().default("rick astley riding a harley through post-apocalyptic miami"), + version: z + .string() + .default("af1a68a271597604546c09c64aabcd7782c114a63539a4a8d14d1eeda5630c33"), + }), + }), + run: async (payload, io, ctx) => { + //wait for prediction completion (uses remote callbacks internally) + const prediction = await io.replicate.predictions.createAndAwait("await-prediction", { + version: payload.version, + input: { + prompt: `${payload.prompt}, cinematic, 70mm, anamorphic, bokeh`, + width: 1280, + height: 720, + }, + }); + return prediction.output; + }, +}); +``` + +### Pagination + +You can paginate responses: + +- Using the `getAll` helper +- Using the `paginate` helper + +```ts +client.defineJob({ + id: "replicate-pagination", + name: "Replicate Pagination", + version: "0.1.0", + integrations: { + replicate, + }, + trigger: eventTrigger({ + name: "replicate.paginate", + }), + run: async (payload, io, ctx) => { + // getAll - returns an array of all results (uses paginate internally) + const all = await io.replicate.getAll(io.replicate.predictions.list, "get-all"); + + // paginate - returns an async generator, useful to process one page at a time + for await (const predictions of io.replicate.paginate( + io.replicate.predictions.list, + "paginate-all" + )) { + await io.logger.info("stats", { + total: predictions.length, + versions: predictions.map((p) => p.version), + }); + } + + return { count: all.length }; + }, +}); +``` + +## Tasks + +### Collections + +| Function Name | Description | +| ------------------ | ---------------------------------------------------------------------- | +| `collections.get` | Gets a collection. | +| `collections.list` | Returns the first page of all collections. Use with pagination helper. | + +### Deployments + +| Function Name | Description | +| ---------------------------------------- | --------------------------------------------------------- | +| `deployments.predictions.create` | Creates a new prediction with a deployment. | +| `deployments.predictions.createAndAwait` | Creates and waits for a new prediction with a deployment. | + +### Models + +| Function Name | Description | +| ----------------- | ------------------------ | +| `models.get` | Gets a model. | +| `models.versions` | Gets a model version. | +| `models.versions` | Gets all model versions. | + +### Predictions + +| Function Name | Description | +| ---------------------------- | ---------------------------------------------------------------------- | +| `predictions.cancel` | Cancels a prediction. | +| `predictions.create` | Creates a prediction. | +| `predictions.createAndAwait` | Creates and waits for a prediction. | +| `predictions.get` | Gets a prediction. | +| `predictions.list` | Returns the first page of all predictions. Use with pagination helper. | + +### Trainings + +| Function Name | Description | +| -------------------------- | -------------------------------------------------------------------- | +| `trainings.cancel` | Cancels a training. | +| `trainings.create` | Creates a training. | +| `trainings.createAndAwait` | Creates and waits for a training. | +| `trainings.get` | Gets a training. | +| `trainings.list` | Returns the first page of all trainings. Use with pagination helper. | + +### Misc + +| Function Name | Description | +| ------------- | --------------------------------------------------- | +| `getAll` | Pagination helper that returns an array of results. | +| `paginate` | Pagination helper that returns an async generator. | +| `request` | Sends authenticated requests to the Replicate API. | +| `run` | Creates and waits for a prediction. | diff --git a/docs/integrations/apis/supabase/introduction.mdx b/docs/integrations/apis/supabase/introduction.mdx index 92485b6c58..af78305dcd 100644 --- a/docs/integrations/apis/supabase/introduction.mdx +++ b/docs/integrations/apis/supabase/introduction.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Supabase: Introduction" +sidebarTitle: "Introduction" --- diff --git a/docs/integrations/create-publishing.mdx b/docs/integrations/create-publishing.mdx index 062b57f173..a70c9d7bb1 100644 --- a/docs/integrations/create-publishing.mdx +++ b/docs/integrations/create-publishing.mdx @@ -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. diff --git a/docs/integrations/create-tasks.mdx b/docs/integrations/create-tasks.mdx index fe5386a181..2ed3b09dfc 100644 --- a/docs/integrations/create-tasks.mdx +++ b/docs/integrations/create-tasks.mdx @@ -24,7 +24,7 @@ export class Github implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/docs/integrations/create-testing.mdx b/docs/integrations/create-testing.mdx index cd5e5a81b5..b7cded7720 100644 --- a/docs/integrations/create-testing.mdx +++ b/docs/integrations/create-testing.mdx @@ -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. diff --git a/docs/integrations/create.mdx b/docs/integrations/create.mdx index 5ae14721da..6d56309ca0 100644 --- a/docs/integrations/create.mdx +++ b/docs/integrations/create.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Create an Integration: Introduction" +sidebarTitle: "Introduction" description: "You can create Integrations of your own." --- diff --git a/docs/integrations/introduction.mdx b/docs/integrations/introduction.mdx index 1496d2e69a..cce19f3a48 100644 --- a/docs/integrations/introduction.mdx +++ b/docs/integrations/introduction.mdx @@ -1,5 +1,6 @@ --- -title: Introduction +title: "Integrations: Introduction" +sidebarTitle: "Introduction" description: "Integrations make it easy to authenticate and use APIs." --- @@ -30,13 +31,15 @@ description: "Integrations make it easy to authenticate and use APIs." Navigate the menu or select Integrations from the table below. -| API | Description | Webhooks | Tasks | -| --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- | -| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | βœ… | βœ… | -| [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 | πŸ•˜ | βœ… | -| [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | πŸ•˜ | βœ… | -| [Slack](/integrations/apis/slack) | Send Slack messages | πŸ•˜ | βœ… | -| [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | βœ… | βœ… | -| [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | βœ… | βœ… | +| 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 | πŸ•˜ | βœ… | +| [Replicate](/integrations/apis/replicate) | Run machine learning tasks easily at scale | N/A | βœ… | +| [Resend](/integrations/apis/resend) | Send emails using Resend | πŸ•˜ | βœ… | +| [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | πŸ•˜ | βœ… | +| [Slack](/integrations/apis/slack) | Send Slack messages | πŸ•˜ | βœ… | +| [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | βœ… | βœ… | +| [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | βœ… | βœ… | diff --git a/docs/mint.json b/docs/mint.json index f877bcf3d6..1d0e4dba85 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -142,6 +142,7 @@ "group": "Manual setup", "pages": [ "documentation/guides/manual/nextjs", + "documentation/guides/manual/nestjs", "documentation/guides/manual/express", "documentation/guides/manual/remix", "documentation/guides/manual/redwood", @@ -151,7 +152,6 @@ "documentation/guides/manual/fastify" ] }, - "documentation/guides/running-jobs", "documentation/guides/jobs/managing", { @@ -168,10 +168,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,8 +245,10 @@ "integrations/apis/github-tasks" ] }, + "integrations/apis/linear", "integrations/apis/openai", "integrations/apis/plain", + "integrations/apis/replicate", "integrations/apis/resend", "integrations/apis/sendgrid", "integrations/apis/slack", @@ -268,8 +278,13 @@ "pages": [ "sdk/triggerclient/instancemethods/sendevent", "sdk/triggerclient/instancemethods/getevent", + "sdk/triggerclient/instancemethods/cancel-event", "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 +318,10 @@ "sdk/dynamictrigger/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"] + "pages": [ + "sdk/dynamictrigger/register", + "sdk/dynamictrigger/unregister" + ] } ] }, @@ -314,7 +332,10 @@ "sdk/dynamicschedule/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"] + "pages": [ + "sdk/dynamicschedule/register", + "sdk/dynamicschedule/unregister" + ] } ] }, @@ -335,7 +356,9 @@ }, { "group": "Overview", - "pages": ["examples/introduction"] + "pages": [ + "examples/introduction" + ] } ], "footerSocials": { @@ -348,4 +371,4 @@ "apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW" } } -} +} \ No newline at end of file diff --git a/docs/sdk/dynamicschedule/overview.mdx b/docs/sdk/dynamicschedule/overview.mdx index 4525aac74c..2216bc92dc 100644 --- a/docs/sdk/dynamicschedule/overview.mdx +++ b/docs/sdk/dynamicschedule/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "DynamicSchedule: Overview" +sidebarTitle: "Overview" description: "`DynamicSchedule` allows you to define a scheduled trigger that can be configured dynamically at runtime." --- @@ -34,7 +35,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 +77,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); }, }); ``` diff --git a/docs/sdk/dynamicschedule/register.mdx b/docs/sdk/dynamicschedule/register.mdx index 71b9cd76c5..f5f0a83949 100644 --- a/docs/sdk/dynamicschedule/register.mdx +++ b/docs/sdk/dynamicschedule/register.mdx @@ -7,8 +7,8 @@ description: "Use this method to register a new schedule with the DynamicSchedul ## Parameters - The id of the schedule to register. The identifier you use will be available - in the `context.source.id` when the Job runs. + The id of the schedule to register. The identifier you use will be available in the + `context.source.id` when the Job runs. The schedule to register. It is either a `cron` or `interval` schedule. @@ -24,8 +24,13 @@ description: "Use this method to register a new schedule with the DynamicSchedul - Any additional data you wish to store with the schedule. This will be - available in the `context.source.metadata` when the Job runs. + Any additional data you wish to store with the schedule. This will be available in the + `context.source.metadata` when the Job runs. + + + An optional account ID to use when running the job. This will be available in the Job + [context](/sdk/context) and can be used in [auth + resolvers](/sdk/triggerclient/instancemethods/define-auth-resolver) @@ -40,8 +45,13 @@ description: "Use this method to register a new schedule with the DynamicSchedul - Any additional data you wish to store with the schedule. This will be - available in the `context.source.metadata` when the Job runs. + Any additional data you wish to store with the schedule. This will be available in the + `context.source.metadata` when the Job runs. + + + An optional account ID to use when running the job. This will be available in the Job + [context](/sdk/context) and can be used in [auth + resolvers](/sdk/triggerclient/instancemethods/define-auth-resolver) @@ -77,7 +87,7 @@ description: "Use this method to register a new schedule with the DynamicSchedul An object containing options about the interval. - The number of seconds for the interval. Min = 60, Max = 86400 (1 day) + The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days) diff --git a/docs/sdk/dynamictrigger/constructor.mdx b/docs/sdk/dynamictrigger/constructor.mdx index 477d264e10..aea8078d00 100644 --- a/docs/sdk/dynamictrigger/constructor.mdx +++ b/docs/sdk/dynamictrigger/constructor.mdx @@ -16,9 +16,8 @@ description: "The `DynamicTrigger()` constructor creates a new [DynamicTrigger]( Used to uniquely identify a DynamicTrigger - An event from an [Integration](/integrations) package that you want to - attach to the DynamicTrigger. The event types will come through to the - payload in your Job's run. + An event from an [Integration](/integrations) package that you want to attach to the + DynamicTrigger. The event types will come through to the payload in your Job's run. An external source fron an [Integration](/integrations) package diff --git a/docs/sdk/dynamictrigger/overview.mdx b/docs/sdk/dynamictrigger/overview.mdx index c6626d90ac..c597b3783c 100644 --- a/docs/sdk/dynamictrigger/overview.mdx +++ b/docs/sdk/dynamictrigger/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "DynamicTrigger: Overview" +sidebarTitle: "Overview" description: "`DynamicTrigger` allows you to define a trigger that can be configured dynamically at runtime." --- @@ -9,7 +10,7 @@ Sometimes you want to subscribe to a webhook but you don't know the exact config ### [DynamicTrigger()](/sdk/dynamictrigger/constructor) -Creates a new `DynamicTrigger` instance. +Creates a new `DynamicTrigger` instance. You should use the [`TriggerClient.defineDynamicTrigger`]() method instead of calling this directly. ## Instance methods @@ -33,7 +34,7 @@ Use this method to unregister a schedule from the DynamicTrigger, using the id y ```typescript DynamicTrigger //1. create a DynamicTrigger -const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, { +const dynamicOnIssueOpenedTrigger = client.defineDynamicTrigger({ id: "github-issue-opened", event: events.onIssueOpened, source: github.sources.repo, @@ -59,7 +60,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, }); @@ -76,16 +77,13 @@ client.defineJob({ org: "triggerdotdev", }), 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, - } - ); + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + //6. Register the dynamic trigger so you get notified when an issue is opened. A task will automatically be created + await dynamicOnIssueOpenedTrigger.register(`${owner}-${repo}`, { + owner, + repo, + }); }, }); ``` diff --git a/docs/sdk/dynamictrigger/register.mdx b/docs/sdk/dynamictrigger/register.mdx index 18136e7a71..95e2049c8c 100644 --- a/docs/sdk/dynamictrigger/register.mdx +++ b/docs/sdk/dynamictrigger/register.mdx @@ -7,12 +7,25 @@ description: "Use this method to register a new configuration with the DynamicTr ## Parameters - The id of the registration. The identifier you use will be available in the - `context.source.id` when the Job runs. It will also be used to unregister. + The id of the registration. The identifier you use will be available in the `context.source.id` + when the Job runs. It will also be used to unregister. - The shape of this object will depend on the type of event you set when - constructing the `DynamicTrigger`. + The shape of this object will depend on the type of event you set when constructing the + `DynamicTrigger`. + + + + + An optional account ID to use when running the job. This will be available in the Job + [context](/sdk/context) and can be used in [auth + resolvers](/sdk/triggerclient/instancemethods/define-auth-resolver) + + + An optional filter to apply to the event. See our [EventFilter + guide](/documentation/guides/event-filter) for more + + ## Returns diff --git a/docs/sdk/intervaltrigger.mdx b/docs/sdk/intervaltrigger.mdx index 727317cb68..6cea27ecbd 100644 --- a/docs/sdk/intervaltrigger.mdx +++ b/docs/sdk/intervaltrigger.mdx @@ -13,7 +13,7 @@ Intervals are set with a number of seconds. There are some important considerati - The Job will first run the specified number of seconds after it has first connected to an [Environment](/documentation/concepts/environments-endpoints). This will happen when you first [deploy](/documentation/guides/deployment) that Job. - The minimum interval is 60 seconds (any input less than this it will default to 60). -- The maximum interval is 86400 seconds (24 hours), if you pass more than this it will trigger every 24 hours. +- The maximum interval is 2_592_000 seconds (30 days), if you pass more than this it will trigger every 30 days. If you wish to Run a Job at an exact time or less frequently than once pr day you should use a [cronTrigger()](/sdk/crontrigger) instead. @@ -22,7 +22,7 @@ If you wish to Run a Job at an exact time or less frequently than once pr day yo - The number of seconds for the interval. Min = 60, Max = 86400 (1 day) + The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days) diff --git a/docs/sdk/introduction.mdx b/docs/sdk/introduction.mdx index 1e2c7076ee..0d4b283621 100644 --- a/docs/sdk/introduction.mdx +++ b/docs/sdk/introduction.mdx @@ -1,5 +1,6 @@ --- -title: "Introduction" +title: "SDK: Introduction" +sidebarTitle: "Introduction" description: "The SDK is how you interact with Trigger.dev" --- diff --git a/docs/sdk/io/overview.mdx b/docs/sdk/io/overview.mdx index 3a061e3366..3c0eb15c3f 100644 --- a/docs/sdk/io/overview.mdx +++ b/docs/sdk/io/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "IO: Overview" +sidebarTitle: "Overview" description: "The second parameter in a Job's `run()` function. It holds Integrations and useful actions you can perform." --- diff --git a/docs/sdk/io/registercron.mdx b/docs/sdk/io/registercron.mdx index ab9bc9708a..769acc0913 100644 --- a/docs/sdk/io/registercron.mdx +++ b/docs/sdk/io/registercron.mdx @@ -4,6 +4,10 @@ sidebarTitle: "registerCron()" description: "`io.registerCron()` allows you to register a [DynamicSchedule](/sdk/dynamicschedule) that will trigger any jobs it's attached to on a regular CRON schedule." --- + + This has been deprecated in favor of [DynamicSchedule.register](/sdk/dynamicschedule/register) + + ## Parameters diff --git a/docs/sdk/io/registerinterval.mdx b/docs/sdk/io/registerinterval.mdx index 48ef44bbf3..1678f549b4 100644 --- a/docs/sdk/io/registerinterval.mdx +++ b/docs/sdk/io/registerinterval.mdx @@ -4,6 +4,10 @@ sidebarTitle: "registerInterval()" description: "`io.registerInterval()` allows you to register a [DynamicSchedule](/sdk/dynamicschedule) that will trigger any jobs it's attached to on a regular interval." --- + + This has been deprecated in favor of [DynamicSchedule.register](/sdk/dynamicschedule/register) + + ## Parameters @@ -19,7 +23,7 @@ description: "`io.registerInterval()` allows you to register a [DynamicSchedule] - The number of seconds for the interval. Min = 60, Max = 86400 (1 day) + The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days) @@ -47,7 +51,7 @@ A Promise that resolves to an object with the following fields: An object containing options about the interval. - The number of seconds for the interval. Min = 60, Max = 86400 (1 day) + The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days) diff --git a/docs/sdk/io/registertrigger.mdx b/docs/sdk/io/registertrigger.mdx index e3bf1fb029..6562ef8cef 100644 --- a/docs/sdk/io/registertrigger.mdx +++ b/docs/sdk/io/registertrigger.mdx @@ -4,6 +4,10 @@ sidebarTitle: "registerTrigger()" description: "`io.registerTrigger()` allows you to register a [DynamicTrigger](/sdk/dynamictrigger) with the specified trigger data." --- + + This has been deprecated in favor of [DynamicTrigger.register](/sdk/dynamictrigger/register) + + ## Parameters @@ -30,9 +34,9 @@ A Promise that resolves to an object with the following fields: ## Example -```typescript +```ts //1. create a DynamicTrigger -const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, { +const dynamicOnIssueOpenedTrigger = client.defineDynamicTrigger({ id: "github-issue-opened", event: events.onIssueOpened, source: github.sources.repo, diff --git a/docs/sdk/io/runtask.mdx b/docs/sdk/io/runtask.mdx index 28be391f19..46a8d829d9 100644 --- a/docs/sdk/io/runtask.mdx +++ b/docs/sdk/io/runtask.mdx @@ -6,6 +6,8 @@ description: "`io.runTask()` allows you to run a [Task](/documentation/concepts/ A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. +The wrappers at `io.integration.runTask()` expose the underlying Integration client as the first callback parameter (see examples on the right). They will have defaults set for options and `onError` handlers, but should otherwise be considered identical to raw `io.runTask()`. + ## Parameters @@ -82,9 +84,12 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged. - - The input params to the Task, will be displayed in the logs. - +{" "} + + + The input params to the Task, will be displayed in the logs. + + The style of the log entry. @@ -98,6 +103,33 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged. + + + An optional object that specifies which fields to redact from the logs. This is useful for sensitive data like API keys. + + + + An array of paths to redact. A path is a dot separated string, e.g. `user.email`. Currently does not support wildcards. + + + + + + + An optional object that exposes settings for the remote callback feature. + + Enabling this feature will expose a `callbackUrl` property on the callback's Task parameter. Additionally, `io.runTask()` will now return a Promise that resolves with the body of the first request sent to that URL. + + + + Whether to enable the remote callback feature. + + + The value of the property. + + + + @@ -119,6 +151,8 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged. A Promise that resolves with the returned value of the callback. +If the remote callback feature `options.callback` is enabled, the Promise will instead resolve with the body of the first request sent to `task.callbackUrl`. + ```typescript Run a task @@ -136,11 +170,11 @@ client.defineJob({ }, run: async (payload, io, ctx) => { //runTask - const response = await io.runTask( + const response = await io.github.runTask( "create-card", - async () => { + async (client) => { //create a project card using the underlying GitHub Integration client - return io.github.client.rest.projects.createCard({ + return client.rest.projects.createCard({ column_id: 123, note: "test", }); @@ -187,4 +221,43 @@ client.defineJob({ }); ``` +```typescript Remote callbacks +client.defineJob({ + id: "remote-callback-example", + name: "Remote Callback example", + version: "0.1.1", + trigger: eventTrigger({ name: "predict" }), + integrations: { replicate }, + run: async (payload, io, ctx) => { + //runTask + const prediction = await io.replicate.runTask( + "create-and-await-prediction", + async (client, task) => { + //create a prediction using the underlying Replicate Integration client + await client.predictions.create({ + ...payload, + webhook: task.callbackUrl ?? "", + webhook_events_filter: ["completed"], + }); + //the actual return value will be the data sent to callbackUrl + //cast to the exact data type you expect to receive or `any` if unsure + return {} as Prediction; + }, + { + name: "Create and await Prediction", + icon: "replicate", + //remote callback settings + callback: { + enabled: true, + timeoutInSeconds: 300, + }, + } + ); + + //log the prediction output + await io.logger.info(prediction.output); + }, +}); +``` + diff --git a/docs/sdk/io/sendevent.mdx b/docs/sdk/io/sendevent.mdx index b68c575853..77df08f72f 100644 --- a/docs/sdk/io/sendevent.mdx +++ b/docs/sdk/io/sendevent.mdx @@ -12,10 +12,6 @@ Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events. - - The number of seconds to wait. This can be very long, serverless timeouts are not an issue. - - ## Returns @@ -24,7 +20,7 @@ Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events. -```typescript Send an event +```ts Send an event //this Job sends an event that triggers the second job client.defineJob({ id: "job-1", diff --git a/docs/sdk/io/unregistercron.mdx b/docs/sdk/io/unregistercron.mdx index 7f750efef2..9b43e0e166 100644 --- a/docs/sdk/io/unregistercron.mdx +++ b/docs/sdk/io/unregistercron.mdx @@ -4,6 +4,10 @@ sidebarTitle: "unregisterCron()" description: "`io.unregisterCron()` allows you to unregister a [DynamicSchedule](/sdk/dynamicschedule) that was previously registered with `io.registerCron()`." --- + + This has been deprecated in favor of [DynamicSchedule.unregister](/sdk/dynamicschedule/unregister) + + ## Parameters diff --git a/docs/sdk/io/unregisterinterval.mdx b/docs/sdk/io/unregisterinterval.mdx index 86d0ff9bd7..7b5fcf3d25 100644 --- a/docs/sdk/io/unregisterinterval.mdx +++ b/docs/sdk/io/unregisterinterval.mdx @@ -4,6 +4,10 @@ sidebarTitle: "unregisterInterval()" description: "`io.unregisterInterval()` allows you to unregister a [DynamicSchedule](/sdk/dynamicschedule) that was previously registered with `io.registerInterval()`." --- + + This has been deprecated in favor of [DynamicSchedule.unregister](/sdk/dynamicschedule/unregister) + + ## Parameters diff --git a/docs/sdk/io/unregistertrigger.mdx b/docs/sdk/io/unregistertrigger.mdx index ca47e9541d..3fa9fa2c2f 100644 --- a/docs/sdk/io/unregistertrigger.mdx +++ b/docs/sdk/io/unregistertrigger.mdx @@ -4,6 +4,10 @@ sidebarTitle: "unregisterTrigger()" description: "`io.unregisterTrigger()` allows you to unregister a [DynamicTrigger](/sdk/dynamictrigger) that was previously registered with `io.registerTrigger()`." --- + + This has been deprecated in favor of [DynamicTrigger.unregister](/sdk/dynamictrigger/unregister) + + ## Parameters diff --git a/docs/sdk/job.mdx b/docs/sdk/job.mdx index a9bc27f818..229bbf13c5 100644 --- a/docs/sdk/job.mdx +++ b/docs/sdk/job.mdx @@ -59,55 +59,7 @@ client.defineJob({ An instance of [TriggerClient](/sdk/triggerclient) that is used to send events to the Trigger API. - - - - The `id` property is used to uniquely identify the Job. Only change this if you want to create a new Job. - - - The `name` of the Job that you want to appear in the dashboard and logs. You can change this without creating a new Job. - - - 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`. - - - 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. - - - 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. - - - 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: - - - - 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`. - - - 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 - - - - + ## Returns diff --git a/docs/sdk/react/introduction.mdx b/docs/sdk/react/introduction.mdx index 63c67ec93f..5c255d15c4 100644 --- a/docs/sdk/react/introduction.mdx +++ b/docs/sdk/react/introduction.mdx @@ -1,5 +1,6 @@ --- -title: "Introduction" +title: "React SDK: Introduction" +sidebarTitle: "Introduction" description: "The React SDK allows you to display the status of your Jobs and Runs in your React app." --- diff --git a/docs/sdk/triggerclient/instancemethods/cancel-event.mdx b/docs/sdk/triggerclient/instancemethods/cancel-event.mdx new file mode 100644 index 0000000000..0702b6d6e6 --- /dev/null +++ b/docs/sdk/triggerclient/instancemethods/cancel-event.mdx @@ -0,0 +1,31 @@ +--- +title: "TriggerClient: cancelEvent() instance method" +sidebarTitle: "cancelEvent()" +description: "The `cancelEvent()` instance method will cancel an event that is scheduled to be delivered in the future." +--- + +If you've scheduled an event to be delivered in the future, you can cancel it using the `cancelEvent()` instance method, passing in the ID of the event you want to cancel. This will prevent any jobs listening for that event from being triggered. + +```ts +// Sending an event that will be delivered in 24 hours +const event = await client.sendEvent( + { + id: "event_12345", + name: "my.event", + payload: { + foo: "bar", + }, + }, + { + deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // deliver in 24 hours + } +); + +// Sometime later, cancel the event by ID +await client.cancelEvent(event.id); +``` + + + Cancelling an event after it has already triggered a job run does not cancel the job run. + Cancelling events only prevent the event from triggering future job runs. + diff --git a/docs/sdk/triggerclient/instancemethods/define-auth-resolver.mdx b/docs/sdk/triggerclient/instancemethods/define-auth-resolver.mdx new file mode 100644 index 0000000000..b6f34a577b --- /dev/null +++ b/docs/sdk/triggerclient/instancemethods/define-auth-resolver.mdx @@ -0,0 +1,67 @@ +--- +title: "defineAuthResolver()" +description: "Define a custom auth resolver for a specific integration" +--- + +Auth Resolvers allow you to inject the authentication credentials of **your users**, using a third-party service like [Clerk](https://clerk.com/) or [Nango](https://www.nango.dev/) or your own custom solution. + +See our [Bring-your-own Auth Guide](/documentation/guides/using-integrations-byo-auth) for more about how this works. + + + +```ts example +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, + }; +}); +``` + + + +## Parameters + + + The Integration client (e.g. `slack`) to define the auth resolver for. + + + + The resolver function to use for this integration. Should return a [AuthResolverResult](#authresolverresult) object. + +{" "} + + + + The [TriggerContext](/sdk/context) object for the run that is requesting authentication. + + + The Integration client that is requesting authentication. + + + + + +## AuthResolverResult + + + Should be either "apiKey" or "oauth" + + + + The authentication token to use for this integration. + + + + Additional fields to pass to the integration. + diff --git a/docs/sdk/triggerclient/instancemethods/define-dynamic-schedule.mdx b/docs/sdk/triggerclient/instancemethods/define-dynamic-schedule.mdx new file mode 100644 index 0000000000..3509ce38e3 --- /dev/null +++ b/docs/sdk/triggerclient/instancemethods/define-dynamic-schedule.mdx @@ -0,0 +1,29 @@ +--- +title: "defineDynamicSchedule()" +description: "Define a Dynamic Schedule" +--- + +## Parameters + + + The options for the dynamic schedule. + + + Used to uniquely identify a DynamicSchedule + + + + +## Returns + + + + + +```ts example +const dynamicSchedule = client.defineDynamicSchedule({ + id: "dynamicinterval", +}); +``` + + diff --git a/docs/sdk/triggerclient/instancemethods/define-dynamic-trigger.mdx b/docs/sdk/triggerclient/instancemethods/define-dynamic-trigger.mdx new file mode 100644 index 0000000000..42dd6b850c --- /dev/null +++ b/docs/sdk/triggerclient/instancemethods/define-dynamic-trigger.mdx @@ -0,0 +1,38 @@ +--- +title: "defineDynamicTrigger()" +description: "Define a Dynamic Trigger" +--- + +## Parameters + + + The options for the dynamic trigger. + + + Used to uniquely identify a DynamicTrigger + + + An event from an [Integration](/integrations) package that you want to attach to the + DynamicTrigger. The event types will come through to the payload in your Job's run. + + + An external source fron an [Integration](/integrations) package + + + + +## Returns + + + + + +```ts example +const dynamicOnIssueOpenedTrigger = client.defineDynamicTrigger({ + id: "github-issue-opened", + event: events.onIssueOpened, + source: github.sources.repo, +}); +``` + + diff --git a/docs/sdk/triggerclient/instancemethods/define-job.mdx b/docs/sdk/triggerclient/instancemethods/define-job.mdx new file mode 100644 index 0000000000..2b9732e1b0 --- /dev/null +++ b/docs/sdk/triggerclient/instancemethods/define-job.mdx @@ -0,0 +1,35 @@ +--- +title: "defineJob()" +description: "Defines a job" +--- + +A [Job](/documentation/concepts/jobs) is used to define the [Trigger](/documentation/concepts/triggers), metadata, and what happens when it runs. + + + +```ts example +client.defineJob({ + id: "github-integration-on-issue", + name: "GitHub Integration - On Issue", + version: "0.1.0", + trigger: github.triggers.repo({ + event: events.onIssue, + owner: "triggerdotdev", + repo: "empty", + }), + run: async (payload, io, ctx) => { + await io.logger.info("This is a simple log info message"); + return { payload, ctx }; + }, +}); +``` + + + +## Parameters + + + +## Returns + + diff --git a/docs/sdk/triggerclient/instancemethods/sendevent.mdx b/docs/sdk/triggerclient/instancemethods/sendevent.mdx index ba2c393b70..05c0b4bf88 100644 --- a/docs/sdk/triggerclient/instancemethods/sendevent.mdx +++ b/docs/sdk/triggerclient/instancemethods/sendevent.mdx @@ -4,7 +4,7 @@ sidebarTitle: "sendEvent()" description: "The `sendEvent()` instance method send an event that triggers any Jobs that are listening for that event (based on the name)." --- -You can call this function from anywhere in your code to send an event. The other way to send an event is by using [io.sendEvent()](/sdk/io/sendevent) from inside a `run()` function. +You can call this function from anywhere in your backend to send an event. The other way to send an event is by using [io.sendEvent()](/sdk/io/sendevent) from inside a `run()` function. Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events. diff --git a/docs/sdk/triggerclient/overview.mdx b/docs/sdk/triggerclient/overview.mdx index c9484812aa..6d263dd0bf 100644 --- a/docs/sdk/triggerclient/overview.mdx +++ b/docs/sdk/triggerclient/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Overview" +title: "TriggerClient: Overview" +sidebarTitle: "Overview" description: "TriggerClient is used to create a client that connects to the Trigger.dev platform" --- @@ -12,6 +13,11 @@ export const client = new TriggerClient({ }); ``` + + The `TriggerClient` should only ever be used in a server-side environment. It is not safe to use + in a browser environment because it exposes your API Key. + + ## Constructor ### [TriggerClient()](/sdk/triggerclient/constructor) @@ -36,6 +42,10 @@ You can call this function from anywhere in your code to send an event. The othe The `getEvent()` method gets the event details for a given eventId. +#### [cancelEvent()](/sdk/triggerclient/instancemethods/cancel-event) + +The `cancelEvent()` method cancels an event that is scheduled to be delivered in the future. + #### [getRuns()](/sdk/triggerclient/instancemethods/getruns) The `getRuns()` method gets runs for a Job. @@ -43,3 +53,19 @@ The `getRuns()` method gets runs for a Job. #### [getRun()](/sdk/triggerclient/instancemethods/getrun) The `getRun()` method gets the details for a given Run. + +#### [defineJob()](/sdk/triggerclient/instancemethods/define-job) + +The `defineJob()` method defines a new Job. + +#### [defineDynamicTrigger()](/sdk/triggerclient/instancemethods/define-dynamic-trigger) + +The `defineDynamicTrigger()` method defines a new Dynamic Trigger. + +#### [defineDynamicSchedule()](/sdk/triggerclient/instancemethods/define-dynamic-schedule) + +The `defineDynamicSchedule()` method defines a new Dynamic Schedule. + +#### [defineAuthResolver()](/sdk/triggerclient/instancemethods/define-auth-resolver) + +The `defineAuthResolver()` method defines a new Auth Resolver. diff --git a/examples/fastify-example/package.json b/examples/fastify-example/package.json new file mode 100644 index 0000000000..32f5e0b566 --- /dev/null +++ b/examples/fastify-example/package.json @@ -0,0 +1,29 @@ +{ + "name": "fastify-example", + "version": "0.0.1", + "description": "", + "main": "src/server.ts", + "scripts": { + "start": "node ./dist/server.js", + "dev": "ts-node ./src/server.ts", + "build": "tsc" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@trigger.dev/fastify": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "dotenv": "^16.3.1", + "fastify": "^4.23.0" + }, + "devDependencies": { + "@types/node": "^20.6.0", + "ts-node": "^10.9.1", + "typescript": "^5.2.2", + "@trigger.dev/cli": "workspace:*" + }, + "trigger.dev": { + "endpointId": "fastify-example" + } +} diff --git a/examples/fastify-example/src/server.ts b/examples/fastify-example/src/server.ts new file mode 100644 index 0000000000..6500073e67 --- /dev/null +++ b/examples/fastify-example/src/server.ts @@ -0,0 +1,16 @@ +import fastify from "fastify"; +import { createMiddleware } from "@trigger.dev/fastify"; +import { client } from "./trigger"; +import "dotenv/config"; + +const app = fastify({ + logger: true, +}); + +const middleware = createMiddleware(client); + +app.addHook("preHandler", middleware); + +app.listen({ port: 3000 }, () => { + console.log("Listening on port 3000"); +}); diff --git a/examples/fastify-example/src/trigger.ts b/examples/fastify-example/src/trigger.ts new file mode 100644 index 0000000000..2f3410f2f7 --- /dev/null +++ b/examples/fastify-example/src/trigger.ts @@ -0,0 +1,27 @@ +import { TriggerClient } from "@trigger.dev/sdk"; +import { eventTrigger } from "@trigger.dev/sdk"; + +export const client = new TriggerClient({ + id: "fastify-example", + apiKey: process.env.TRIGGER_API_KEY!, + apiUrl: process.env.TRIGGER_API_URL!, + logLevel: "debug", + ioLogLocalEnabled: true, + verbose: true, +}); + +client.defineJob({ + id: "fastify-example", + name: "Fastify example", + version: "0.0.1", + trigger: eventTrigger({ + name: "Fastify Test Run", + }), + run: async (_payload, io, _ctx) => { + await io.wait("wait", 15); + await io.logger.info("Hello Fastify!"); + return { + message: "Trigger x Fastify", + }; + }, +}); diff --git a/examples/fastify-example/tsconfig.json b/examples/fastify-example/tsconfig.json new file mode 100644 index 0000000000..df23c4bc92 --- /dev/null +++ b/examples/fastify-example/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "strict": true, + "allowJs": true, + "module": "commonjs", + "declaration": true, + "target": "ES2019", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": ".", + "strictNullChecks": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": false + }, + "include": ["./src/**/*.ts"] +} diff --git a/examples/jobs-starter/README.md b/examples/jobs-starter/README.md deleted file mode 100644 index f4da3c4c1c..0000000000 --- a/examples/jobs-starter/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/jobs-starter/app/layout.tsx b/examples/jobs-starter/app/layout.tsx deleted file mode 100644 index cce852fd75..0000000000 --- a/examples/jobs-starter/app/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import "./globals.css"; -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} diff --git a/examples/jobs-starter/app/page.tsx b/examples/jobs-starter/app/page.tsx deleted file mode 100644 index 75ceab209f..0000000000 --- a/examples/jobs-starter/app/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
-

- Get started by editing  - app/page.tsx -

- -
- -
- Next.js Logo -
- - -
- ); -} diff --git a/examples/jobs-starter/jobs/starter.ts b/examples/jobs-starter/jobs/starter.ts deleted file mode 100644 index c92d592a4b..0000000000 --- a/examples/jobs-starter/jobs/starter.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { eventTrigger, intervalTrigger } from "@trigger.dev/sdk"; -import { client } from "@/trigger"; - -const QUOTES = [ - "Any fool can write code that a computer can understand. Good programmers write code that humans can understand. – Martin Fowler", - "First, solve the problem. Then, write the code. – John Johnson", - "Experience is the name everyone gives to their mistakes. – Oscar Wilde", - "In order to be irreplaceable, one must always be different – Coco Chanel", - "Knowledge is power. – Francis Bacon", - "Sometimes it pays to stay in bed on Monday, rather than spending the rest of the week debugging Monday’s code. – Dan Salomon", - "Perfection is achieved not when there is nothing more to add, but rather when there is nothing more to take away. – Antoine de Saint-Exupery", - "Rust is the most loved programming language. – Stack Overflow", - "Code is like humor. When you have to explain it, it’s bad. – Cory House", - "Fix the cause, not the symptom. – Steve Maguire", -]; - -client.defineJob({ - id: "hello-world", - name: "Hello World", - version: "0.0.1", - trigger: eventTrigger({ - name: "starter.hello-world", - }), - run: async (_payload, io, _ctx) => { - await io.logger.info("Hello world!"); - - return { - message: "Hello world!", - }; - }, -}); - -client.defineJob({ - id: "quote", - name: "Random Quote", - version: "0.0.1", - trigger: eventTrigger({ - name: "starter.quote", - }), - run: async (_payload, _io, _ctx) => { - return { - quote: QUOTES[Math.floor(Math.random() * QUOTES.length)], - }; - }, -}); - -client.defineJob({ - id: "usd-eur-change-rate", - name: "USD-EUR Change Rate", - version: "0.0.1", - trigger: intervalTrigger({ - seconds: 60, - }), - run: async (_payload, io, _ctx) => { - // This is just a dummy example, we can actually use an API to pull the actual rate - await io.logger.info("Fetching USD-EUR rate..."); - - return { - rate: 0.85, - }; - }, -}); - -client.defineJob({ - id: "stars-count", - name: "Number of stars for Trigger.dev repo", - version: "0.0.1", - trigger: intervalTrigger({ - seconds: 60 * 60, - }), - run: async (_payload, io, _ctx) => { - return await io.runTask( - "get-stars-count", - - async () => { - try { - const response = await fetch("https://api.github.com/repos/triggerdotdev/trigger.dev"); - const { stargazers_count } = await response.json(); - - return { success: true, stargazers_count }; - } catch (error) { - await io.logger.error("Failed to fetch stars count", { error }); - - return { success: false }; - } - }, - { name: "Get Trigger.dev stars count" } - ); - }, -}); diff --git a/examples/jobs-starter/next.config.js b/examples/jobs-starter/next.config.js deleted file mode 100644 index 658404ac69..0000000000 --- a/examples/jobs-starter/next.config.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {}; - -module.exports = nextConfig; diff --git a/examples/jobs-starter/package.json b/examples/jobs-starter/package.json deleted file mode 100644 index 8732b02043..0000000000 --- a/examples/jobs-starter/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@examples/jobs-starter", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "concurrently --kill-others npm:dev:*", - "dev:next": "next dev", - "dev:trigger": "npx @trigger.dev/cli dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@trigger.dev/nextjs": "workspace:*", - "@trigger.dev/sdk": "workspace:*", - "@types/node": "20.4.2", - "@types/react": "18.2.17", - "@types/react-dom": "18.2.7", - "autoprefixer": "10.4.14", - "eslint": "8.45.0", - "eslint-config-next": "13.4.10", - "next": "13.4.10", - "postcss": "8.4.26", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwindcss": "3.3.3", - "typescript": "5.1.6" - }, - "trigger.dev": { - "endpointId": "jobs-starter" - }, - "devDependencies": { - "concurrently": "^8.2.0", - "@trigger.dev/cli": "workspace:*" - } -} diff --git a/examples/jobs-starter/tsconfig.json b/examples/jobs-starter/tsconfig.json deleted file mode 100644 index 52c2f4f284..0000000000 --- a/examples/jobs-starter/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2015", - "lib": ["dom", "dom.iterable", "ES2015"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": ["./*"], - "@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"], - "@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"], - "@trigger.dev/nextjs": ["../../packages/nextjs/src/index"], - "@trigger.dev/nextjs/*": ["../../packages/nextjs/src/*"] - }, - "plugins": [ - { - "name": "next" - } - ] - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/examples/nextjs-12/.gitignore b/examples/nextjs-12/.gitignore deleted file mode 100644 index c87c9b392c..0000000000 --- a/examples/nextjs-12/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/examples/nextjs-12/README.md b/examples/nextjs-12/README.md deleted file mode 100644 index b12f3e33e7..0000000000 --- a/examples/nextjs-12/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/nextjs-12/jsconfig.json b/examples/nextjs-12/jsconfig.json deleted file mode 100644 index 875cb60012..0000000000 --- a/examples/nextjs-12/jsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "compilerOptions": {} -} diff --git a/examples/nextjs-12/next.config.js b/examples/nextjs-12/next.config.js deleted file mode 100644 index 3d3bc9990d..0000000000 --- a/examples/nextjs-12/next.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - swcMinify: true, -}; - -module.exports = nextConfig; diff --git a/examples/nextjs-12/package.json b/examples/nextjs-12/package.json deleted file mode 100644 index b82e8c2bd0..0000000000 --- a/examples/nextjs-12/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "nextjs-12", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "next": "12.3.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "@trigger.dev/nextjs": "workspace:*", - "@trigger.dev/sdk": "workspace:*" - }, - "devDependencies": { - "eslint": "8.44.0", - "eslint-config-next": "12.3.4", - "@trigger.dev/cli": "workspace:*" - }, - "trigger.dev": { - "endpointId": "nextjs-12" - } -} diff --git a/examples/nextjs-12/pages/_app.js b/examples/nextjs-12/pages/_app.js deleted file mode 100644 index 2fc3e07009..0000000000 --- a/examples/nextjs-12/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import "../styles/globals.css"; - -function MyApp({ Component, pageProps }) { - return ; -} - -export default MyApp; diff --git a/examples/nextjs-12/pages/api/hello.js b/examples/nextjs-12/pages/api/hello.js deleted file mode 100644 index aee21e9afa..0000000000 --- a/examples/nextjs-12/pages/api/hello.js +++ /dev/null @@ -1,5 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction - -export default function handler(req, res) { - res.status(200).json({ name: "John Doe" }); -} diff --git a/examples/nextjs-12/pages/api/trigger.js b/examples/nextjs-12/pages/api/trigger.js deleted file mode 100644 index b5539162a6..0000000000 --- a/examples/nextjs-12/pages/api/trigger.js +++ /dev/null @@ -1,16 +0,0 @@ -import { TriggerClient } from "@trigger.dev/sdk"; -import { createPagesRoute } from "@trigger.dev/nextjs"; - -export const client = new TriggerClient({ - id: "nextjs-12", - apiKey: process.env.TRIGGER_API_KEY, - apiUrl: process.env.TRIGGER_API_URL, - verbose: false, - ioLogLocalEnabled: true, -}); - -const { handler, config } = createPagesRoute(client); - -export { config }; - -export default handler; diff --git a/examples/nextjs-12/pages/index.js b/examples/nextjs-12/pages/index.js deleted file mode 100644 index 32694edba4..0000000000 --- a/examples/nextjs-12/pages/index.js +++ /dev/null @@ -1,63 +0,0 @@ -import Head from "next/head"; -import Image from "next/image"; -import styles from "../styles/Home.module.css"; - -export default function Home() { - return ( - - ); -} diff --git a/examples/nextjs-12/public/vercel.svg b/examples/nextjs-12/public/vercel.svg deleted file mode 100644 index fbf0e25a65..0000000000 --- a/examples/nextjs-12/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/examples/nextjs-12/styles/Home.module.css b/examples/nextjs-12/styles/Home.module.css deleted file mode 100644 index 90817bd391..0000000000 --- a/examples/nextjs-12/styles/Home.module.css +++ /dev/null @@ -1,138 +0,0 @@ -.container { - padding: 0 2rem; -} - -.main { - min-height: 100vh; - padding: 4rem 0; - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.footer { - display: flex; - flex: 1; - padding: 2rem 0; - border-top: 1px solid #eaeaea; - justify-content: center; - align-items: center; -} - -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; -} - -.title a { - color: #0070f3; - text-decoration: none; -} - -.title a:hover, -.title a:focus, -.title a:active { - text-decoration: underline; -} - -.title { - margin: 0; - line-height: 1.15; - font-size: 4rem; -} - -.title, -.description { - text-align: center; -} - -.description { - margin: 4rem 0; - line-height: 1.5; - font-size: 1.5rem; -} - -.code { - background: #fafafa; - border-radius: 5px; - padding: 0.75rem; - font-size: 1.1rem; - font-family: - Menlo, - Monaco, - Lucida Console, - Liberation Mono, - DejaVu Sans Mono, - Bitstream Vera Sans Mono, - Courier New, - monospace; -} - -.grid { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - max-width: 800px; -} - -.card { - margin: 1rem; - padding: 1.5rem; - text-align: left; - color: inherit; - text-decoration: none; - border: 1px solid #eaeaea; - border-radius: 10px; - transition: - color 0.15s ease, - border-color 0.15s ease; - max-width: 300px; -} - -.card:hover, -.card:focus, -.card:active { - color: #0070f3; - border-color: #0070f3; -} - -.card h2 { - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.card p { - margin: 0; - font-size: 1.25rem; - line-height: 1.5; -} - -.logo { - height: 1em; - margin-left: 0.5rem; -} - -@media (max-width: 600px) { - .grid { - width: 100%; - flex-direction: column; - } -} - -@media (prefers-color-scheme: dark) { - .card, - .footer { - border-color: #222; - } - .code { - background: #111; - } - .logo img { - filter: invert(1); - } -} diff --git a/examples/nextjs-12/styles/globals.css b/examples/nextjs-12/styles/globals.css deleted file mode 100644 index f7efe20c36..0000000000 --- a/examples/nextjs-12/styles/globals.css +++ /dev/null @@ -1,36 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: - -apple-system, - BlinkMacSystemFont, - Segoe UI, - Roboto, - Oxygen, - Ubuntu, - Cantarell, - Fira Sans, - Droid Sans, - Helvetica Neue, - sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } - body { - color: white; - background: black; - } -} diff --git a/examples/nextjs-example/src/app/api/trigger/route.ts b/examples/nextjs-example/src/app/api/trigger/route.ts deleted file mode 100644 index f72f543aed..0000000000 --- a/examples/nextjs-example/src/app/api/trigger/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createAppRoute } from "@trigger.dev/nextjs"; -import { client } from "@/trigger"; - -import "@/jobs/events"; -import "@/jobs/general"; -import "@/jobs/github"; -import "@/jobs/logging"; -import "@/jobs/openai"; -import "@/jobs/plain"; -import "@/jobs/resend"; -import "@/jobs/schedules"; -import "@/jobs/slack"; -import "@/jobs/typeform"; -import "@/jobs/edgeCases"; -import "@/jobs/hooks"; -import "@/jobs/supabase"; -import "@/jobs/stripe"; - -export const { POST, dynamic } = createAppRoute(client); diff --git a/examples/nextjs-example/src/components/RunDetails.tsx b/examples/nextjs-example/src/components/RunDetails.tsx deleted file mode 100644 index d6a83dbf1f..0000000000 --- a/examples/nextjs-example/src/components/RunDetails.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import styles from "@/styles/Home.module.css"; -import { useEventDetails, useEventRunDetails, useRunDetails } from "@trigger.dev/react"; - -export function EventData({ id }: { id: string }) { - const { isLoading, data, error } = useEventDetails(id); - - return ( - <> -

Event

- {isLoading ? ( -

Loading

- ) : error ? ( - JSON.stringify(error, null, 2) - ) : data ? ( -

- Event ID: {data.id} -

- ) : ( -

- )} - - ); -} - -export function RunData({ id }: { id: string }) { - const { isLoading, isError, data, error } = useRunDetails(id); - - if (isLoading) { - return

Loading...

; - } - - if (isError) { - return

Error

; - } - - if (!data) { - return

Loading...

; - } - - return ( - <> -
Run status: {data.status}
-
- {data.tasks?.map((task) => ( -
-

{task.name}

-

Status: {task.status}

-
- ))} -
- - ); -} - -export function EventRunData({ id }: { id: string }) { - const { isLoading, isError, data, error } = useEventRunDetails(id); - - if (isLoading) { - return

Loading...

; - } - - if (isError) { - return

Error

; - } - - if (!data) { - return

Loading...

; - } - - return ( - <> -
Run status: {data.status}
-
- {data.tasks?.map((task) => ( -
-

{task.displayKey ?? task.name}

-

{task.icon}

-

Status: {task.status}

-
- ))} -
- {data.output && ( - -
{JSON.stringify(data.output, null, 2)}
-
- )} - - ); -} diff --git a/examples/nextjs-example/src/jobs/edgeCases.ts b/examples/nextjs-example/src/jobs/edgeCases.ts deleted file mode 100644 index cd7c60b23e..0000000000 --- a/examples/nextjs-example/src/jobs/edgeCases.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { client } from "@/trigger"; -import { Job, eventTrigger } from "@trigger.dev/sdk"; -import { z } from "zod"; - -client.defineJob({ - id: "test-long-running-cpu", - name: "Test long running CPU", - version: "0.0.1", - trigger: eventTrigger({ - name: "test.cpu", - schema: z.object({ - iterations: z.number(), - sleepDuration: z.number(), - }), - }), - run: async (payload, io, ctx) => { - console.log(`Running run ${ctx.run.id} at ${new Date().toISOString()}`); - - for (let i = 0; i < payload.iterations ?? 1; i++) { - await new Promise((resolve) => setTimeout(resolve, payload.sleepDuration ?? 1000)); - } - - console.log(`Finishing run ${ctx.run.id} at ${new Date().toISOString()}`); - }, -}); diff --git a/examples/nextjs-example/src/jobs/events.ts b/examples/nextjs-example/src/jobs/events.ts deleted file mode 100644 index 4ef4071864..0000000000 --- a/examples/nextjs-example/src/jobs/events.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { client } from "@/trigger"; -import { Job, eventTrigger } from "@trigger.dev/sdk"; -import { z } from "zod"; - -client.defineJob({ - id: "test-event-trigger-1", - name: "Test Event Trigger 1", - version: "0.0.1", - logLevel: "debug", - trigger: eventTrigger({ - name: "test-event-trigger-1", - schema: z.object({ - name: z.string(), - payload: z.any(), - }), - }), - run: async (payload, io, ctx) => { - await io.sendEvent( - "send", - { - name: payload.name, - payload: payload.payload, - timestamp: new Date(), - }, - { deliverAt: new Date(Date.now() + 1000 * 30) } - ); - }, -}); - -client.defineJob({ - id: "test-event-trigger-2", - name: "Test Event Trigger 2", - version: "0.0.1", - logLevel: "debug", - trigger: eventTrigger({ - name: "test-event-trigger-2", - }), - run: async (payload, io, ctx) => { - for (let index = 0; index < 100; index++) { - await io.sendEvent(`send-${index}`, { - name: "test-event-trigger-1", - payload: { name: "whatever", payload: { index } }, - }); - } - }, -}); - -client.defineJob({ - id: "test-multiple-events", - name: "Test Multiple Events", - version: "0.0.1", - logLevel: "debug", - trigger: eventTrigger({ - name: ["test.event.1", "test.event.2"], - examples: [{ id: "test", name: "Test", payload: { name: "test" } }], - }), - run: async (payload, io, ctx) => { - await io.logger.log(`Triggered by the ${ctx.event.name} event`, { ctx }); - }, -}); diff --git a/examples/nextjs-example/src/jobs/general.ts b/examples/nextjs-example/src/jobs/general.ts deleted file mode 100644 index a35786bf9f..0000000000 --- a/examples/nextjs-example/src/jobs/general.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { client, github, githubUser, openai, slack } from "@/trigger"; -import { events } from "@trigger.dev/github"; -import { - Job, - cronTrigger, - eventTrigger, - intervalTrigger, - isTriggerError, - missingConnectionNotification, - missingConnectionResolvedNotification, -} from "@trigger.dev/sdk"; -import { z } from "zod"; - -const enabled = true; - -client.defineJob({ - id: "on-missing-auth-connection", - name: "On missing auth connection", - version: "0.1.1", - enabled, - trigger: missingConnectionNotification([githubUser]), - integrations: { - slack, - }, - run: async (payload, io, ctx) => { - switch (payload.type) { - case "DEVELOPER": { - return await io.slack.postMessage("message", { - text: `Missing developer connection: ${JSON.stringify(payload)}`, - channel: "C04GWUTDC3W", - }); - } - case "EXTERNAL": { - return await io.slack.postMessage("message", { - text: `Missing external connection: account: ${JSON.stringify( - payload.account - )}, payload: ${JSON.stringify(payload)}`, - channel: "C04GWUTDC3W", - }); - } - } - }, -}); - -client.defineJob({ - id: "on-missing-auth-connection-resolved", - name: "On missing auth connection-resolved", - version: "0.1.1", - enabled, - trigger: missingConnectionResolvedNotification([githubUser]), - integrations: { - slack, - }, - run: async (payload, io, ctx) => { - switch (payload.type) { - case "DEVELOPER": { - return await io.slack.postMessage("message", { - text: `Missing developer connection resolved: ${JSON.stringify(payload)}`, - channel: "C04GWUTDC3W", - }); - } - case "EXTERNAL": { - return await io.slack.postMessage("message", { - text: `Missing external connection resolved: ${JSON.stringify(payload)}`, - channel: "C04GWUTDC3W", - }); - } - } - }, -}); - -client.defineJob({ - id: "get-user-repo", - name: "Get User Repo", - version: "0.1.1", - enabled, - trigger: eventTrigger({ - name: "get.repo", - schema: z.object({ - owner: z.string(), - repo: z.string(), - }), - }), - integrations: { - github: githubUser, - }, - run: async (payload, io, ctx) => { - return await io.github.getRepo("get.repo", payload); - }, -}); - -client.defineJob({ - id: "event-1", - name: "Run when the foo.bar event happens", - version: "0.0.1", - enabled: true, - trigger: eventTrigger({ - name: "foo.bar", - }), - run: async (payload, io, ctx) => { - await io.try( - async () => { - return await io.runTask( - "task-1", - async (task) => { - if (task.attempts > 2) { - return { - bar: "foo", - }; - } - - throw new Error(`Task failed on ${task.attempts} attempt(s)`); - }, - { name: "task-1", retry: { limit: 3 } } - ); - }, - async (error) => { - // These should never be reached - await io.wait("wait-after-error", 5); - - await io.logger.error("This is a log error message", { - payload, - error, - }); - - return { - foo: "bar", - }; - } - ); - - try { - await io.runTask( - "task-2", - async (task) => { - throw new Error(`Task failed on ${task.attempts} attempt(s)`); - }, - { name: "task-2", retry: { limit: 5 } } - ); - } catch (error) { - if (isTriggerError(error)) { - throw error; - } - - await io.wait("wait-after-error", 5); - - await io.logger.error("This is a log error message", { - payload, - error, - }); - } - - return { - payload, - }; - }, -}); - -client.defineJob({ - id: "scheduled-job-1", - name: "Scheduled Job 1", - version: "0.1.1", - enabled: true, - trigger: intervalTrigger({ - seconds: 60, - }), - run: async (payload, io, ctx) => { - await io.wait("wait", 5); // wait for 5 seconds - - await io.logger.info("This is a log info message", { - payload, - }); - - await io.sendEvent("send-event", { - name: "custom.event", - payload, - context: ctx, - }); - - await io.runTask("level 1", async () => { - await io.runTask("level 2", async () => { - await io.runTask("level 3", async () => { - await io.runTask("level 4", async () => { - await io.runTask("level 5", async () => {}); - }); - }); - }); - }); - - await io.wait("5 minutes", 5 * 60); - - await io.runTask("Fingers crossed", async () => { - throw new Error("You messed up buddy!"); - }); - }, -}); - -client.defineJob({ - id: "scheduled-job-2", - name: "Scheduled Job 2", - version: "0.1.1", - enabled, - trigger: cronTrigger({ - cron: "*/5 * * * *", // every 5 minutes - }), - run: async (payload, io, ctx) => { - await io.wait("wait", 5); // wait for 5 seconds - await io.logger.info("This is a log info message", { - payload, - ctx, - }); - - return { - message: "Hello from scheduled job 1", - }; - }, -}); - -client.defineJob({ - id: "test-io-functions", - name: "Test IO functions", - version: "0.1.1", - enabled, - trigger: eventTrigger({ - name: "test.io", - }), - run: async (payload, io, ctx) => { - await io.wait("wait", 5); // wait for 5 seconds - await io.logger.info("This is a log info message", { - payload, - }); - await io.sendEvent("send-event", { - name: "custom.event", - payload, - context: ctx, - }); - }, -}); - -client.defineJob({ - id: "alert-on-new-github-issues-3", - name: "Alert on new GitHub issues", - version: "0.1.1", - enabled, - integrations: { - slack, - }, - trigger: github.triggers.repo({ - event: events.onIssueOpened, - owner: "ericallam", - repo: "basic-starter-12k", - }), - run: async (payload, io, ctx) => { - await io.runTask("slow task", async () => { - await new Promise((resolve) => setTimeout(resolve, 5000)); - }); - - await io.logger.info("This is a simple log info message"); - - await io.wait("wait", 5); // wait for 5 seconds - - const response = await io.slack.postMessage("Slack πŸ“", { - text: `New Issue opened: ${payload.issue.html_url}`, - channel: "C04GWUTDC3W", - }); - - return response; - }, -}); diff --git a/examples/nextjs-example/src/jobs/github.ts b/examples/nextjs-example/src/jobs/github.ts deleted file mode 100644 index c4aa833b4e..0000000000 --- a/examples/nextjs-example/src/jobs/github.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { client, github, slack } from "@/trigger"; -import { Github } from "@trigger.dev/github"; -import { events } from "@trigger.dev/github"; -import { Job } from "@trigger.dev/sdk"; - -const githubApiKey = new Github({ - id: "github-api-key", - token: process.env["GITHUB_API_KEY"]!, -}); - -client.defineJob({ - id: "github-integration-on-issue", - name: "GitHub Integration - On Issue", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onIssue, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-issue-opened", - name: "GitHub Integration - On Issue Opened", - version: "0.1.0", - integrations: { github: githubApiKey }, - trigger: githubApiKey.triggers.repo({ - event: events.onIssueOpened, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.github.addIssueAssignees("add assignee", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - assignees: ["matt-aitken"], - }); - - await io.github.addIssueLabels("add label", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - labels: ["bug"], - }); - - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "new-github-issue-reminder", - name: "New GitHub issue reminder", - version: "0.1.0", - integrations: { github, slack }, - trigger: github.triggers.repo({ - event: events.onIssueOpened, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - //delay for 24 hours (or 60 seconds in development) - const delayDuration = ctx.environment.type === "DEVELOPMENT" ? 60 : 60 * 60 * 24; - await io.wait("wait 24 hours", delayDuration); - - const issue = await io.github.getIssue("get issue", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - }); - - //if the issue has had no activity - if (issue.updated_at === payload.issue.updated_at) { - await io.slack.postMessage("Slack reminder", { - text: `New issue needs attention: <${issue.html_url}|${issue.title}>`, - channel: "C04GWUTDC3W", - }); - - //assign it to someone, in this case… me - await io.github.addIssueAssignees("add assignee", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - assignees: ["ericallam"], - }); - } - }, -}); - -client.defineJob({ - id: "github-integration-on-issue-assigned", - name: "GitHub Integration - On Issue assigned", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onIssueAssigned, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-issue-commented", - name: "GitHub Integration - On Issue commented", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onIssueComment, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "star-slack-notification", - name: "New Star Slack Notification", - version: "0.1.0", - integrations: { slack }, - trigger: githubApiKey.triggers.repo({ - event: events.onNewStar, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - const response = await io.slack.postMessage("Slack star", { - text: `${payload.sender.login} starred ${payload.repository.full_name}.\nTotal: ${payload.repository.stargazers_count}⭐️`, - channel: "C04GWUTDC3W", - }); - }, -}); - -client.defineJob({ - id: "github-integration-on-new-star", - name: "GitHub Integration - On New Star", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onNewStar, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-new-repo", - name: "GitHub Integration - On New Repository", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onNewRepository, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-new-branch-or-tag", - name: "GitHub Integration - On New Branch or Tag", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onNewBranchOrTag, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-new-branch", - name: "GitHub Integration - On New Branch", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onNewBranch, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-push", - name: "GitHub Integration - On Push", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onPush, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-pull-request", - name: "GitHub Integration - On Pull Request", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onPullRequest, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-pull-request-review", - name: "GitHub Integration - On Pull Request Review", - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onPullRequestReview, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-on-pull-request-merge-commit", - name: "GitHub Integration - on Pull Request Merge Commit", - version: "0.1.0", - integrations: { github }, - trigger: githubApiKey.triggers.repo({ - event: events.onPullRequest, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - - if (payload.pull_request.merged && payload.pull_request.merge_commit_sha) { - const commit = await io.github.getCommit("get merge commit", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - commitSHA: payload.pull_request.merge_commit_sha, - }); - await io.logger.info("Merge Commit Details", commit); - } - - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-get-tree", - name: "GitHub Integration - Get Tree", - version: "0.1.0", - integrations: { github }, - trigger: githubApiKey.triggers.repo({ - event: events.onPullRequest, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - - if (payload.pull_request.merged && payload.pull_request.merge_commit_sha) { - const tree = await io.github.getTree("get merge commit", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - treeSHA: payload.pull_request.merge_commit_sha, - }); - await io.logger.info("Tree ", tree); - } - - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-get-reference", - name: "GitHub Integration - Get Reference", - integrations: { github }, - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onNewBranch, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - - const ref = await io.github.getReference("Get reference", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - ref: payload.ref, - }); - - await io.logger.info("Reference ", ref); - - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-list-matching-references", - name: "GitHub Integration - List Matching References", - integrations: { github }, - version: "0.1.0", - trigger: githubApiKey.triggers.repo({ - event: events.onNewBranch, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - - const ref = await io.github.listMatchingReferences("List Matching References", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - ref: payload.ref, - }); - - await io.logger.info("Reference ", ref); - - return { payload, ctx }; - }, -}); - -client.defineJob({ - id: "github-integration-get-tag", - name: "GitHub Integration - Get Tag", - version: "0.1.0", - integrations: { github }, - trigger: githubApiKey.triggers.repo({ - event: events.onNewBranchOrTag, - owner: "triggerdotdev", - repo: "empty", - }), - run: async (payload, io, ctx) => { - await io.logger.info("This is a simple log info message"); - if (payload.ref_type === "tag") { - const tag = io.github.getTag("Get Tag", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - tagSHA: payload.ref, - }); - await io.logger.info("Tag ", tag); - } - return { payload, ctx }; - }, -}); diff --git a/examples/nextjs-example/src/jobs/hooks.ts b/examples/nextjs-example/src/jobs/hooks.ts deleted file mode 100644 index f7a9fd425d..0000000000 --- a/examples/nextjs-example/src/jobs/hooks.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { client } from "@/trigger"; -import { eventTrigger } from "@trigger.dev/sdk"; - -client.defineJob({ - id: "hooks-test-job", - name: "Hooks test job", - version: "0.1.1", - trigger: eventTrigger({ - name: "test-event", - }), - run: async (payload, io, ctx) => { - await io.logger.log("This Job is triggered from a button in the frontend"); - await io.wait("wait", 5); - await io.logger.log("It runs for a while to test the React hooks"); - await io.wait("wait 2", 5); - await io.logger.log("This is the end of the job"); - - return { - myMessage: "This is the output of the job", - }; - }, -}); diff --git a/examples/nextjs-example/src/jobs/logging.ts b/examples/nextjs-example/src/jobs/logging.ts deleted file mode 100644 index 3ebd97c5b8..0000000000 --- a/examples/nextjs-example/src/jobs/logging.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { client } from "@/trigger"; -import { Job, eventTrigger } from "@trigger.dev/sdk"; - -client.defineJob({ - id: "test-logging", - name: "Test logging", - version: "0.0.1", - logLevel: "debug", - trigger: eventTrigger({ - name: "test.logging", - }), - run: async (payload, io, ctx) => { - await io.logger.log("Hello log level", { payload }); - await io.logger.error("Hello error level", { payload }); - await io.logger.warn("Hello warn level", { payload }); - await io.logger.info("Hello info level", { payload }); - await io.logger.debug("Hello debug level", { payload }); - }, -}); diff --git a/examples/nextjs-example/src/jobs/openai.ts b/examples/nextjs-example/src/jobs/openai.ts deleted file mode 100644 index cd3219fa5d..0000000000 --- a/examples/nextjs-example/src/jobs/openai.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { client } from "@/trigger"; -import { OpenAI } from "@trigger.dev/openai"; -import { Job, eventTrigger } from "@trigger.dev/sdk"; -import { z } from "zod"; - -const openai = new OpenAI({ - id: "openai", - apiKey: process.env["OPENAI_API_KEY"]!, -}); - -client.defineJob({ - id: "openai-tasks", - name: "OpenAI Tasks", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.tasks", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const models = await io.openai.listModels("list-models"); - - if (models.data.length > 0) { - await io.openai.retrieveModel("get-model", { - model: models.data[0].id, - }); - } - - await io.openai.backgroundCreateChatCompletion("background-chat-completion", { - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: "Create a good programming joke about background jobs", - }, - ], - }); - - await io.openai.createChatCompletion("chat-completion", { - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: "Create a good programming joke about background jobs", - }, - ], - }); - - await io.openai.backgroundCreateCompletion("background-completion", { - model: "text-davinci-003", - prompt: "Create a good programming joke about Tasks", - }); - - await io.openai.createCompletion("completion", { - model: "text-davinci-003", - prompt: "Create a good programming joke about Tasks", - }); - - await io.openai.createEdit("edit", { - model: "text-davinci-edit-001", - input: "Thsi is ridddled with erors", - instruction: "Fix the spelling errors", - }); - - await io.openai.createEmbedding("embedding", { - model: "text-embedding-ada-002", - input: "The food was delicious and the waiter...", - }); - }, -}); - -client.defineJob({ - id: "openai-images", - name: "OpenAI Images", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.images", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - await io.openai.createImage("image", { - prompt: "A hedgehog wearing a party hat", - n: 2, - size: "256x256", - response_format: "url", - }); - }, -}); - -client.defineJob({ - id: "openai-files", - name: "OpenAI Files", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.files", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - // jsonl string - await io.openai.createFile("file-string", { - file: `{ "prompt": "Tell me a joke", "completion": "Something funny" }\n{ "prompt": "Tell me another joke", "completion": "Something also funny" }`, - fileName: "cool-file.jsonl", - purpose: "fine-tune", - }); - - // fine tune file - const fineTuneFile = await io.openai.createFineTuneFile("file-fine-tune", { - fileName: "fine-tune.jsonl", - examples: [ - { - prompt: "Tell me a joke", - completion: "Why did the chicken cross the road? No one knows", - }, - { - prompt: "Tell me another joke", - completion: "Why did the chicken cross the road? To get to the other side", - }, - ], - }); - - const model = await io.openai.createFineTune("fine-tune", { - model: "davinci", - training_file: fineTuneFile.id, - }); - - const fineTunes = await io.openai.listFineTunes("list-fine-tunes"); - - const fineTune = await io.openai.retrieveFineTune("get-fine-tune", { - fineTuneId: model.id, - }); - - const events = await io.openai.listFineTuneEvents("list-fine-tune-events", { - fineTuneId: model.id, - }); - - const cancelFineTune = await io.openai.cancelFineTune("cancel-fine-tune", { - fineTuneId: model.id, - }); - - const files = await io.openai.listFiles("list-files"); - await io.logger.info("files", files); - - //this will fail because the fine tune didn't complete - await io.logger.info("This next task will fail because the model never completed"); - const deleteFineTune = await io.openai.deleteFineTune("delete-fine-tune", { - fineTunedModelId: model.id, - }); - }, -}); diff --git a/examples/nextjs-example/src/jobs/plain.ts b/examples/nextjs-example/src/jobs/plain.ts deleted file mode 100644 index 7eb51b1f8d..0000000000 --- a/examples/nextjs-example/src/jobs/plain.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { client } from "@/trigger"; -import { - ComponentDividerSpacingSize, - ComponentTextColor, - ComponentTextSize, - Plain, -} from "@trigger.dev/plain"; -import { Job, eventTrigger } from "@trigger.dev/sdk"; - -export const plain = new Plain({ - id: "plain-1", - apiKey: process.env["PLAIN_API_KEY"]!, -}); - -client.defineJob({ - id: "plain-playground", - name: "Plain Playground", - version: "0.1.1", - integrations: { - plain, - }, - trigger: eventTrigger({ - name: "plain.playground", - }), - run: async (payload, io, ctx) => { - const { customer } = await io.plain.upsertCustomer("upsert-customer", { - identifier: { - emailAddress: "eric@trigger.dev", - }, - onCreate: { - email: { - email: "eric@trigger.dev", - isVerified: true, - }, - fullName: "Eric Allam", - externalId: "123", - }, - onUpdate: { - fullName: { - value: "Eric Allam", - }, - externalId: { - value: "123", - }, - }, - }); - - const result = await io.plain.runTask("create-issue", async (client) => - client.createIssue({ - customerId: "abcdefghij", - issueTypeId: "123456", - }) - ); - - const foundCustomer = await io.plain.getCustomerById("get-customer", { - customerId: customer.id, - }); - - const timelineEntry = await io.plain.upsertCustomTimelineEntry("upsert-timeline-entry", { - customerId: customer.id, - title: "My timeline entry", - components: [ - { - componentText: { - text: `This is a nice title`, - }, - }, - { - componentDivider: { - dividerSpacingSize: ComponentDividerSpacingSize.M, - }, - }, - { - componentText: { - textSize: ComponentTextSize.S, - textColor: ComponentTextColor.Muted, - text: "External id", - }, - }, - { - componentText: { - text: foundCustomer?.externalId ?? "", - }, - }, - ], - }); - }, -}); diff --git a/examples/nextjs-example/src/jobs/schedules.ts b/examples/nextjs-example/src/jobs/schedules.ts deleted file mode 100644 index d5dc061ebf..0000000000 --- a/examples/nextjs-example/src/jobs/schedules.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { client } from "@/trigger"; -import { Job, cronTrigger } from "@trigger.dev/sdk"; - -client.defineJob({ - id: "test-cron-schedule-5", - name: "Test Cron Schedule 5", - version: "0.0.1", - logLevel: "debug", - trigger: cronTrigger({ - cron: "*/1 * * * *", - }), - run: async (payload, io, ctx) => { - await io.logger.debug("Hello cron schedule 2a", { - payload, - payload2: payload, - }); - }, -}); diff --git a/examples/nextjs-example/src/jobs/slack.ts b/examples/nextjs-example/src/jobs/slack.ts deleted file mode 100644 index 4df5200607..0000000000 --- a/examples/nextjs-example/src/jobs/slack.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { client } from "@/trigger"; -import { Slack } from "@trigger.dev/slack"; -import { Job, cronTrigger, eventTrigger } from "@trigger.dev/sdk"; - -const db = { - getKpiSummary: async (date: Date) => { - return { - revenue: 23_269, - orders: 1_234, - }; - }, -}; - -export const slack = new Slack({ id: "slack-6" }); -export const slackMissing = new Slack({ id: "slack-7" }); - -client.defineJob({ - id: "slack-kpi-summary", - name: "Slack kpi summary", - version: "0.1.1", - integrations: { - slack, - }, - trigger: cronTrigger({ - cron: "0 9 * * *", // 9am every day (UTC) - }), - run: async (payload, io, ctx) => { - const { revenue } = await db.getKpiSummary(payload.ts); - const response = await io.slack.postMessage("Slack πŸ“", { - text: `Yesterday's revenue was $${revenue}`, - channel: "C04GWUTDC3W", - }); - - return response; - }, -}); - -client.defineJob({ - id: "slack-auto-join", - name: "Slack Auto Join", - version: "0.1.1", - integrations: { - slack, - }, - trigger: eventTrigger({ - name: "slack.auto_join", - }), - run: async (payload, io, ctx) => { - const response = await io.slack.postMessage("Slack πŸ“", { - channel: "C05G130TH4G", - text: "Welcome to the team, Eric!", - blocks: [ - { - type: "section", - text: { - type: "mrkdwn", - text: `Welcome to the team, Eric!`, - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: `I'm here to help you get started with Trigger!`, - }, - }, - ], - }); - - return response; - }, -}); - -client.defineJob({ - id: "slack-missing-integration", - name: "Slack with missing integration", - version: "0.1.1", - integrations: { - slack: slackMissing, - }, - trigger: eventTrigger({ - name: "missing.integration", - }), - run: async (payload, io, ctx) => { - const response = await io.slack.postMessage("message", { - text: `There's no Slack connection, or is there?`, - channel: "C04GWUTDC3W", - }); - - return response; - }, -}); diff --git a/examples/nextjs-example/src/jobs/stripe.ts b/examples/nextjs-example/src/jobs/stripe.ts deleted file mode 100644 index df834b82a3..0000000000 --- a/examples/nextjs-example/src/jobs/stripe.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Stripe } from "@trigger.dev/stripe"; -import { client } from "@/trigger"; -import { eventTrigger } from "@trigger.dev/sdk"; -import { z } from "zod"; - -const stripe = new Stripe({ - id: "stripe", - apiKey: process.env["STRIPE_API_KEY"]!, -}); - -client.defineJob({ - id: "stripe-example-1", - name: "Stripe Example 1", - version: "0.1.0", - trigger: eventTrigger({ - name: "stripe.example", - schema: z.object({ - customerId: z.string(), - source: z.string(), - }), - }), - integrations: { - stripe, - }, - run: async (payload, io, ctx) => { - await io.stripe.createCharge("create-charge", { - amount: 100, - currency: "usd", - source: payload.source, - customer: payload.customerId, - }); - }, -}); - -client.defineJob({ - id: "stripe-example-1", - name: "Stripe Example 1", - version: "0.1.0", - trigger: eventTrigger({ - name: "stripe.example", - schema: z.object({ - customerId: z.string(), - source: z.string(), - }), - }), - integrations: { - stripe, - }, - run: async (payload, io, ctx) => { - await io.stripe.createCharge("create-charge", { - amount: 100, - currency: "usd", - source: payload.source, - customer: payload.customerId, - }); - }, -}); - -client.defineJob({ - id: "stripe-create-customer", - name: "Stripe Create Customer", - version: "0.1.0", - trigger: eventTrigger({ - name: "stripe.new.customer", - schema: z.object({ - email: z.string(), - name: z.string(), - }), - }), - integrations: { - stripe, - }, - run: async (payload, io, ctx) => { - await io.stripe.createCustomer("create-customer", { - email: payload.email, - name: payload.name, - }); - }, -}); - -client.defineJob({ - id: "stripe-update-customer", - name: "Stripe Update Customer", - version: "0.1.0", - trigger: eventTrigger({ - name: "stripe.update.customer", - schema: z.object({ - customerId: z.string(), - name: z.string(), - }), - }), - integrations: { - stripe, - }, - run: async (payload, io, ctx) => { - await io.stripe.updateCustomer("update-customer", { - id: payload.customerId, - name: payload.name, - }); - }, -}); - -client.defineJob({ - id: "stripe-retrieve-subscription", - name: "Stripe Retrieve Subscription", - version: "0.1.0", - trigger: eventTrigger({ - name: "stripe.retrieve.subscription", - schema: z.object({ - id: z.string(), - }), - }), - integrations: { - stripe, - }, - run: async (payload, io, ctx) => { - const subscription = await io.stripe.retrieveSubscription("get", { - id: payload.id, - expand: ["customer"], - }); - }, -}); - -client.defineJob({ - id: "stripe-on-price", - name: "Stripe On Price", - version: "0.1.0", - trigger: stripe.onPrice({ events: ["price.created", "price.updated"] }), - run: async (payload, io, ctx) => { - if (ctx.event.name === "price.created") { - await io.logger.info("price created!", { ctx }); - } else { - await io.logger.info("price updated!", { ctx }); - } - }, -}); - -client.defineJob({ - id: "stripe-on-price-created", - name: "Stripe On Price Created", - version: "0.1.0", - trigger: stripe.onPriceCreated(), - run: async (payload, io, ctx) => { - await io.logger.info("ctx", { ctx }); - }, -}); - -client.defineJob({ - id: "stripe-on-subscription-created", - name: "Stripe On Subscription Created", - version: "0.1.0", - trigger: stripe.onCustomerSubscriptionCreated({ - filter: { - currency: ["usd"], - }, - }), - run: async (payload, io, ctx) => { - await io.logger.info("ctx", { ctx }); - }, -}); diff --git a/examples/nextjs-example/src/jobs/supabase.ts b/examples/nextjs-example/src/jobs/supabase.ts deleted file mode 100644 index 0b93b3c37a..0000000000 --- a/examples/nextjs-example/src/jobs/supabase.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { Database } from "@/supabase.types"; -import { client } from "@/trigger"; -import { - type IntegrationIO, - Job, - eventTrigger, - type JobPayload, - type JobIO, - type TriggerPayload, - type IOWithIntegrations, -} from "@trigger.dev/sdk"; -import { SupabaseManagement, Supabase } from "@trigger.dev/supabase"; -import { z } from "zod"; - -const supabase = new SupabaseManagement({ - id: "supabase", -}); - -const db = supabase.db(process.env["SUPABASE_ID"]!); - -const dbNoTypes = supabase.db(process.env["SUPABASE_ID"]!); - -const supabaseManagementKey = new SupabaseManagement({ - id: "supabase-management-key", - apiKey: process.env["SUPABASE_API_KEY"]!, -}); - -const dbKey = supabase.db(process.env["SUPABASE_ID"]!); - -const supabaseDB = new Supabase({ - id: "supabase-db", - supabaseUrl: `https://${process.env["SUPABASE_ID"]}.supabase.co`, - supabaseKey: process.env["SUPABASE_KEY"]!, -}); - -async function doPlaygroundStuff(io: IntegrationIO, ref: string) { - await io.getPGConfig("get-pg-config", { - ref, - }); -} - -new Job(client, { - id: "supabase-playground", - name: "Supabase Playground", - version: "0.1.1", - trigger: eventTrigger({ - name: "supabase.playground", - }), - integrations: { - supabase, - supabaseDB, - supabaseManagementKey, - }, - run: async (payload, io, ctx) => { - await io.supabaseManagementKey.getPGConfig("get-pg-config", { - ref: payload.ref, - }); - - await io.supabase.getOrganizations("get-orgs"); - await io.supabase.getProjects("get-projects"); - - await io.supabase.listFunctions("list-functions", { - ref: payload.ref, - }); - - await io.supabase.runQuery("run-query", { - ref: payload.ref, - query: "SELECT * FROM users", - }); - - await io.supabase.getTypescriptTypes("get-typescript-types", { - ref: payload.ref, - }); - - const users = await io.supabaseDB.runTask( - "fetch-users", - async (db) => { - const { data, error } = await db.from("users").select("*"); - - if (error) throw error; - - return data; - }, - { name: "Fetch Users" } - ); - - const newUser = await io.supabaseDB.runTask( - "create-user", - async (db) => { - return await db - .from("users") - .insert({ - first_name: "John", - last_name: "Doe", - email_address: "john@trigger.dev", - }) - .select(); - }, - { name: "New Users" } - ); - }, -}); - -const createTodoJob = new Job(client, { - id: "supabase-create-todo", - name: "Supabase Create Todo", - version: "0.1.1", - trigger: eventTrigger({ - name: "supabase.create-todo", - schema: z.object({ - contents: z.string(), - user_id: z.number(), - }), - }), - integrations: { - supabaseDB, - }, - run: async (payload, io, ctx) => { - const newTodo = await io.supabaseDB.runTask( - "create-todo", - async (db) => { - const { data, error } = await db - .from("todos") - .insert({ - contents: payload.contents, - user_id: payload.user_id, - is_complete: false, - }) - .select(); - - if (error) throw error; - - return data; - }, - { - name: "Create Todo", - properties: [{ label: "Contents", text: payload.contents }], - } - ); - }, -}); - -type CreateTodo = JobPayload; -type CreateTodoIO = JobIO; - -async function runCreateTodo(payload: CreateTodo, io: CreateTodoIO) { - const newTodo = await io.supabaseDB.runTask( - "create-todo", - async (db) => { - const { data, error } = await db - .from("todos") - .insert({ - contents: payload.contents, - user_id: payload.user_id, - is_complete: false, - }) - .select(); - - if (error) throw error; - - return data; - }, - { - name: "Create Todo", - properties: [{ label: "Contents", text: payload.contents }], - } - ); -} - -const createProjectTrigger = eventTrigger({ - name: "supabase.create", - schema: z.object({ - name: z.string(), - organization_id: z.string(), - plan: z.enum(["free", "pro"]), - region: z.enum(["us-east-1", "us-west-1"]), - password: z.string(), - }), -}); - -async function doRun( - payload: TriggerPayload, - io: IOWithIntegrations<{ supabase: typeof supabase }> -) { - await io.supabase.createProject("create-project", { - name: payload.name, - organization_id: payload.organization_id, - plan: payload.plan, - region: payload.region, - kps_enabled: true, - db_pass: payload.password, - }); -} - -new Job(client, { - id: "supabase-create-project", - name: "Supabase Create Project", - version: "0.1.1", - trigger: createProjectTrigger, - integrations: { - supabase, - }, - run: async (payload, io, ctx) => { - await io.supabase.createProject("create-project", { - name: payload.name, - organization_id: payload.organization_id, - plan: payload.plan, - region: payload.region, - kps_enabled: true, - db_pass: payload.password, - }); - }, -}); - -new Job(client, { - id: "supabase-on-user-insert", - name: "Supabase On User Insert", - version: "0.1.1", - trigger: db.onInserted({ - table: "users", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-user-insert-2", - name: "Supabase On User Insert 2", - version: "0.1.1", - trigger: db.onInserted({ - table: "users", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-user-email-changed", - name: "Supabase On User Email Changed", - version: "0.1.1", - trigger: db.onUpdated({ - table: "users", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-user-deleted", - name: "Supabase On User Deleted", - version: "0.1.1", - trigger: db.onDeleted({ - table: "users", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-todo-created", - name: "Supabase On TODO created", - version: "0.1.1", - trigger: dbKey.onInserted({ - table: "todos", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-todo-created", - name: "Supabase On TODO created", - version: "0.1.1", - trigger: dbKey.onInserted({ - table: "todos", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-todo-completed", - name: "Supabase On TODO completed", - version: "0.1.1", - trigger: dbKey.onUpdated({ - table: "todos", - filter: { - old_record: { - is_complete: [false], - }, - record: { - is_complete: [true], - }, - }, - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => { - await io.logger.log("Todo Completed", { payload }); - }, -}); - -new Job(client, { - id: "supabase-on-tweet-created-or-deleted", - name: "Supabase On Tweet Created or Deleted", - version: "0.1.1", - trigger: dbKey.onDeleted({ - schema: "public_2", - table: "tweets", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); - -new Job(client, { - id: "supabase-on-todo-created-no-types", - name: "Supabase On TODO created", - version: "0.1.1", - trigger: dbNoTypes.onInserted({ - table: "todos", - }), - integrations: { - supabase, - }, - run: async (payload, io, ctx) => {}, -}); diff --git a/examples/nextjs-example/src/jobs/typeform.ts b/examples/nextjs-example/src/jobs/typeform.ts deleted file mode 100644 index a2b03e0821..0000000000 --- a/examples/nextjs-example/src/jobs/typeform.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { client } from "@/trigger"; -import { Typeform, events } from "@trigger.dev/typeform"; -import { Job, eventTrigger } from "@trigger.dev/sdk"; -import { DynamicTrigger } from "@trigger.dev/sdk"; -import { z } from "zod"; - -export const typeform = new Typeform({ - id: "typeform-1", - token: process.env["TYPEFORM_API_KEY"]!, -}); - -client.defineJob({ - id: "typeform-playground", - name: "Typeform Playground", - version: "0.1.1", - integrations: { - typeform, - }, - trigger: eventTrigger({ - name: "typeform.playground", - schema: z.object({ - formId: z.string().optional(), - }), - }), - run: async (payload, io, ctx) => { - await io.typeform.listForms("list-forms"); - - if (payload.formId) { - const form = await io.typeform.getForm("get-form", { - uid: payload.formId, - }); - - const listResponses = await io.typeform.listResponses("list-responses", { - uid: payload.formId, - pageSize: 50, - }); - - const allResponses = await io.typeform.getAllResponses("get-all-responses", { - uid: payload.formId, - }); - } - - await io.typeform.runTask( - "create-form", - async (client) => { - return client.forms.create({ - data: { - title: "My Form", - fields: [ - { - title: "What is your name?", - type: "short_text", - ref: "name", - }, - { - title: "What is your email?", - type: "email", - ref: "email", - }, - ], - }, - }); - }, - { name: "Create Form" } - ); - }, -}); - -client.defineJob({ - id: "typeform-webhook-2", - name: "Typeform Webhook 2", - version: "0.1.1", - trigger: typeform.onFormResponse({ - uid: "QQnotGJM", - tag: "tag4", - }), - run: async (payload, io, ctx) => {}, -}); - -const dynamicTrigger = new DynamicTrigger(client, { - id: "typeform-dynamic-trigger", - source: typeform.source, - event: events.onFormResponse, -}); diff --git a/examples/nextjs-example/tsconfig.json b/examples/nextjs-example/tsconfig.json deleted file mode 100644 index 28ad34a2cf..0000000000 --- a/examples/nextjs-example/tsconfig.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2015", - "lib": [ - "dom", - "dom.iterable", - "ES2015" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": [ - "./src/*" - ], - "@trigger.dev/sdk": [ - "../../packages/trigger-sdk/src/index" - ], - "@trigger.dev/sdk/*": [ - "../../packages/trigger-sdk/src/*" - ], - "@trigger.dev/nextjs": [ - "../../packages/nextjs/src/index" - ], - "@trigger.dev/nextjs/*": [ - "../../packages/nextjs/src/*" - ], - "@trigger.dev/core": [ - "../../packages/core/src/index" - ], - "@trigger.dev/core/*": [ - "../../packages/core/src/*" - ], - "@trigger.dev/integration-kit": [ - "../../packages/integration-kit/src/index" - ], - "@trigger.dev/integration-kit/*": [ - "../../packages/integration-kit/src/*" - ], - "@trigger.dev/github": [ - "../../integrations/github/src/index" - ], - "@trigger.dev/github/*": [ - "../../integrations/github/src/*" - ], - "@trigger.dev/slack": [ - "../../integrations/slack/src/index" - ], - "@trigger.dev/slack/*": [ - "../../integrations/slack/src/*" - ], - "@trigger.dev/openai": [ - "../../integrations/openai/src/index" - ], - "@trigger.dev/openai/*": [ - "../../integrations/openai/src/*" - ], - "@trigger.dev/resend": [ - "../../integrations/resend/src/index" - ], - "@trigger.dev/resend/*": [ - "../../integrations/resend/src/*" - ], - "@trigger.dev/typeform": [ - "../../integrations/typeform/src/index" - ], - "@trigger.dev/typeform/*": [ - "../../integrations/typeform/src/*" - ], - "@trigger.dev/plain": [ - "../../integrations/plain/src/index" - ], - "@trigger.dev/plain/*": [ - "../../integrations/plain/src/*" - ], - "@trigger.dev/supabase": [ - "../../integrations/supabase/src/index" - ], - "@trigger.dev/supabase/*": [ - "../../integrations/supabase/src/*" - ], - "@trigger.dev/stripe": [ - "../../integrations/stripe/src/index" - ], - "@trigger.dev/stripe/*": [ - "../../integrations/stripe/src/*" - ], - "@trigger.dev/sendgrid": [ - "../../integrations/sendgrid/src/index" - ], - "@trigger.dev/sendgrid/*": [ - "../../integrations/sendgrid/src/*" - ] - }, - "plugins": [ - { - "name": "next" - } - ] - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file diff --git a/examples/nextjs-test/.eslintrc.json b/examples/nextjs-test/.eslintrc.json deleted file mode 100644 index bffb357a71..0000000000 --- a/examples/nextjs-test/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/examples/nextjs-test/src/app/favicon.ico b/examples/nextjs-test/src/app/favicon.ico deleted file mode 100644 index 718d6fea48..0000000000 Binary files a/examples/nextjs-test/src/app/favicon.ico and /dev/null differ diff --git a/examples/package-tester/.eslintrc.json b/examples/package-tester/.eslintrc.json deleted file mode 100644 index bffb357a71..0000000000 --- a/examples/package-tester/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/examples/package-tester/.gitignore b/examples/package-tester/.gitignore deleted file mode 100644 index 8f322f0d8f..0000000000 --- a/examples/package-tester/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/examples/package-tester/app/favicon.ico b/examples/package-tester/app/favicon.ico deleted file mode 100644 index 718d6fea48..0000000000 Binary files a/examples/package-tester/app/favicon.ico and /dev/null differ diff --git a/examples/package-tester/app/globals.css b/examples/package-tester/app/globals.css deleted file mode 100644 index e0936eda7d..0000000000 --- a/examples/package-tester/app/globals.css +++ /dev/null @@ -1,23 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) - rgb(var(--background-start-rgb)); -} diff --git a/examples/package-tester/postcss.config.js b/examples/package-tester/postcss.config.js deleted file mode 100644 index 12a703d900..0000000000 --- a/examples/package-tester/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/examples/package-tester/public/next.svg b/examples/package-tester/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/examples/package-tester/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/package-tester/public/vercel.svg b/examples/package-tester/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/examples/package-tester/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/package-tester/tailwind.config.js b/examples/package-tester/tailwind.config.js deleted file mode 100644 index bb0a4e29fc..0000000000 --- a/examples/package-tester/tailwind.config.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - }, - }, - plugins: [], -}; diff --git a/integrations/airtable/CHANGELOG.md b/integrations/airtable/CHANGELOG.md index b00a8e55c9..20c708da16 100644 --- a/integrations/airtable/CHANGELOG.md +++ b/integrations/airtable/CHANGELOG.md @@ -1,5 +1,67 @@ # @trigger.dev/airtable +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + +## 2.1.7 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.7 +- @trigger.dev/sdk@2.1.7 + +## 2.1.6 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.6 +- @trigger.dev/sdk@2.1.6 + +## 2.1.5 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.5 +- @trigger.dev/sdk@2.1.5 + +## 2.1.4 + +### Patch Changes + +- c0dfa804: Add support for Bring Your Own Auth +- Updated dependencies [ad14983e] +- Updated dependencies [15f17d27] +- Updated dependencies [50137a6f] +- Updated dependencies [c0dfa804] + - @trigger.dev/sdk@2.1.4 + - @trigger.dev/integration-kit@2.1.4 + +## 2.1.3 + +### Patch Changes + +- Updated dependencies: + - `@trigger.dev/sdk@2.1.3` + - `@trigger.dev/integration-kit@2.1.3` + ## 2.1.2 ### Patch Changes diff --git a/integrations/airtable/package.json b/integrations/airtable/package.json index eeec49284e..ab2a0e3039 100644 --- a/integrations/airtable/package.json +++ b/integrations/airtable/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/airtable", - "version": "2.1.2", + "version": "2.1.9", "description": "Trigger.dev integration for airtable", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -26,10 +26,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@trigger.dev/integration-kit": "workspace:^2.1.2", - "@trigger.dev/sdk": "workspace:^2.1.2", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "airtable": "^0.12.1", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/airtable/src/base.ts b/integrations/airtable/src/base.ts index 821a56fced..b354bc136c 100644 --- a/integrations/airtable/src/base.ts +++ b/integrations/airtable/src/base.ts @@ -11,13 +11,10 @@ export type AirtableRecordsParams = TableParams<{}>; export type AirtableRecords = Records
; export class Base { - runTask: AirtableRunTask; - baseId: string; - - constructor(runTask: AirtableRunTask, baseId: string) { - this.runTask = runTask; - this.baseId = baseId; - } + constructor( + private runTask: AirtableRunTask, + public baseId: string + ) {} table(tableName: string) { return new Table(this.runTask, this.baseId, tableName); diff --git a/integrations/airtable/src/index.ts b/integrations/airtable/src/index.ts index c4443d7013..b4d9d542cf 100644 --- a/integrations/airtable/src/index.ts +++ b/integrations/airtable/src/index.ts @@ -1,6 +1,7 @@ import { Prettify } from "@trigger.dev/integration-kit"; import { Json, + retry, type ConnectionAuth, type IO, type IOTask, @@ -8,18 +9,10 @@ import { type RunTaskErrorCallback, type RunTaskOptions, type TriggerIntegration, - retry, } from "@trigger.dev/sdk"; import AirtableSDK from "airtable"; import { Base } from "./base"; -import * as events from "./events"; -import { - WebhookChangeType, - WebhookDataType, - Webhooks, - createTrigger, - createWebhookEventSource, -} from "./webhooks"; +import { Webhooks, createWebhookEventSource } from "./webhooks"; export * from "./types"; @@ -33,9 +26,13 @@ export type AirtableIntegrationOptions = { export type AirtableRunTask = InstanceType["runTask"]; export class Airtable implements TriggerIntegration { + // @internal private _options: AirtableIntegrationOptions; + // @internal private _client?: AirtableSDK; + // @internal private _io?: IO; + // @internal private _connectionKey?: string; constructor(options: Prettify) { @@ -95,7 +92,7 @@ export class Airtable implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/airtable/tsconfig.json b/integrations/airtable/tsconfig.json index 9616ac98a0..d1b7924087 100644 --- a/integrations/airtable/tsconfig.json +++ b/integrations/airtable/tsconfig.json @@ -11,7 +11,8 @@ }, "declaration": false, "declarationMap": false, - "baseUrl": "." + "baseUrl": ".", + "stripInternal": true }, "exclude": ["node_modules"] } diff --git a/integrations/github/CHANGELOG.md b/integrations/github/CHANGELOG.md index 545e09e9a4..8179b15ec6 100644 --- a/integrations/github/CHANGELOG.md +++ b/integrations/github/CHANGELOG.md @@ -1,5 +1,67 @@ # @trigger.dev/github +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + +## 2.1.7 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.7 +- @trigger.dev/sdk@2.1.7 + +## 2.1.6 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.6 +- @trigger.dev/sdk@2.1.6 + +## 2.1.5 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.5 +- @trigger.dev/sdk@2.1.5 + +## 2.1.4 + +### Patch Changes + +- c0dfa804: Add support for Bring Your Own Auth +- Updated dependencies [ad14983e] +- Updated dependencies [15f17d27] +- Updated dependencies [50137a6f] +- Updated dependencies [c0dfa804] + - @trigger.dev/sdk@2.1.4 + - @trigger.dev/integration-kit@2.1.4 + +## 2.1.3 + +### Patch Changes + +- Updated dependencies: + - `@trigger.dev/sdk@2.1.3` + - `@trigger.dev/integration-kit@2.1.3` + ## 2.1.2 ### Patch Changes diff --git a/integrations/github/package.json b/integrations/github/package.json index dd7136a2dd..2f3a27f900 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/github", - "version": "2.1.2", + "version": "2.1.9", "description": "The official GitHub integration for Trigger.dev", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,10 +29,10 @@ "@octokit/request": "^6.2.5", "@octokit/request-error": "^4.0.1", "@octokit/webhooks": "^10.4.0", - "@trigger.dev/sdk": "workspace:^2.1.2", - "@trigger.dev/integration-kit": "workspace:^2.1.2", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", "octokit": "^2.0.14", - "zod": "3.21.4" + "zod": "3.22.3" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/github/src/compound.ts b/integrations/github/src/compound.ts index 1e2c8f8a5e..ae8c85f4c3 100644 --- a/integrations/github/src/compound.ts +++ b/integrations/github/src/compound.ts @@ -5,15 +5,11 @@ import { Issues } from "./issues"; import { ReactionContent, Reactions } from "./reactions"; export class Compound { - runTask: GitHubRunTask; - issues: Issues; - reactions: Reactions; - - constructor(runTask: GitHubRunTask, issues: Issues, reactions: Reactions) { - this.runTask = runTask; - this.issues = issues; - this.reactions = reactions; - } + constructor( + private runTask: GitHubRunTask, + public issues: Issues, + public reactions: Reactions + ) {} createIssueCommentWithReaction( key: IntegrationTaskKey, diff --git a/integrations/github/src/git.ts b/integrations/github/src/git.ts index dda8837cd8..34bfd95404 100644 --- a/integrations/github/src/git.ts +++ b/integrations/github/src/git.ts @@ -34,11 +34,7 @@ type TreeType = { }; export class Git { - runTask: GitHubRunTask; - - constructor(runTask: GitHubRunTask) { - this.runTask = runTask; - } + constructor(private runTask: GitHubRunTask) {} createBlob( key: IntegrationTaskKey, diff --git a/integrations/github/src/index.ts b/integrations/github/src/index.ts index 57b8d4a66d..0716e849b5 100644 --- a/integrations/github/src/index.ts +++ b/integrations/github/src/index.ts @@ -68,9 +68,13 @@ export type GitHubReturnType Promise<{ data: K }>, K >; export class Github implements TriggerIntegration { + // @internal private _options: GithubIntegrationOptions; + // @internal private _client?: Octokit; + // @internal private _io?: IO; + // @internal private _connectionKey?: string; _repoSource: ReturnType; @@ -134,7 +138,7 @@ export class Github implements TriggerIntegration { if (!this._io) throw new Error("No IO"); if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { if (!this._client) throw new Error("No client"); diff --git a/integrations/github/src/issues.ts b/integrations/github/src/issues.ts index 5c4f343bef..80eea79556 100644 --- a/integrations/github/src/issues.ts +++ b/integrations/github/src/issues.ts @@ -6,11 +6,7 @@ import { issueProperties, repoProperties } from "./propertyHelpers"; type AddIssueLabels = GitHubReturnType; export class Issues { - runTask: GitHubRunTask; - - constructor(runTask: GitHubRunTask) { - this.runTask = runTask; - } + constructor(private runTask: GitHubRunTask) {} create( key: IntegrationTaskKey, diff --git a/integrations/github/src/orgs.ts b/integrations/github/src/orgs.ts index cd0dbedfe6..495aeb7b4e 100644 --- a/integrations/github/src/orgs.ts +++ b/integrations/github/src/orgs.ts @@ -3,11 +3,7 @@ import { Octokit } from "octokit"; import { GitHubReturnType, GitHubRunTask, onError } from "./index"; export class Orgs { - runTask: GitHubRunTask; - - constructor(runTask: GitHubRunTask) { - this.runTask = runTask; - } + constructor(private runTask: GitHubRunTask) {} updateWebhook( key: IntegrationTaskKey, diff --git a/integrations/github/src/reactions.ts b/integrations/github/src/reactions.ts index 84a6c31f88..e3b3c10d6c 100644 --- a/integrations/github/src/reactions.ts +++ b/integrations/github/src/reactions.ts @@ -15,11 +15,7 @@ export type ReactionContent = | "eyes"; export class Reactions { - runTask: GitHubRunTask; - - constructor(runTask: GitHubRunTask) { - this.runTask = runTask; - } + constructor(private runTask: GitHubRunTask) {} createForIssueComment( key: IntegrationTaskKey, diff --git a/integrations/github/src/repos.ts b/integrations/github/src/repos.ts index ef5b718692..50d5690853 100644 --- a/integrations/github/src/repos.ts +++ b/integrations/github/src/repos.ts @@ -1,15 +1,9 @@ -import { truncate } from "@trigger.dev/integration-kit"; -import { IntegrationTaskKey, Prettify, retry } from "@trigger.dev/sdk"; -import { GitHubReturnType, GitHubRunTask, onError } from "./index"; +import { IntegrationTaskKey } from "@trigger.dev/sdk"; import { Octokit } from "octokit"; -import { issueProperties, repoProperties } from "./propertyHelpers"; +import { GitHubReturnType, GitHubRunTask, onError } from "./index"; export class Repos { - runTask: GitHubRunTask; - - constructor(runTask: GitHubRunTask) { - this.runTask = runTask; - } + constructor(private runTask: GitHubRunTask) {} get( key: IntegrationTaskKey, diff --git a/integrations/github/src/sources.ts b/integrations/github/src/sources.ts index 1996f86259..a65a0a00e1 100644 --- a/integrations/github/src/sources.ts +++ b/integrations/github/src/sources.ts @@ -1,8 +1,7 @@ import { Webhooks } from "@octokit/webhooks"; -import { ExternalSource, TriggerIntegration, HandlerEvent } from "@trigger.dev/sdk"; +import { omit, safeJsonParse } from "@trigger.dev/integration-kit"; import type { Logger } from "@trigger.dev/sdk"; -import { safeJsonParse, omit } from "@trigger.dev/integration-kit"; -import { Octokit } from "octokit"; +import { ExternalSource, HandlerEvent } from "@trigger.dev/sdk"; import { z } from "zod"; import { Github } from "./index"; diff --git a/integrations/github/tsconfig.json b/integrations/github/tsconfig.json index 9616ac98a0..d1b7924087 100644 --- a/integrations/github/tsconfig.json +++ b/integrations/github/tsconfig.json @@ -11,7 +11,8 @@ }, "declaration": false, "declarationMap": false, - "baseUrl": "." + "baseUrl": ".", + "stripInternal": true }, "exclude": ["node_modules"] } diff --git a/integrations/linear/CHANGELOG.md b/integrations/linear/CHANGELOG.md new file mode 100644 index 0000000000..14c507d615 --- /dev/null +++ b/integrations/linear/CHANGELOG.md @@ -0,0 +1,56 @@ +# @trigger.dev/linear + +## 2.1.9 + +### Patch Changes + +- 9a187f9e: upgrade zod to 3.22.3 +- Updated dependencies [9a187f9e] +- Updated dependencies [2e9452ab] + - @trigger.dev/sdk@2.1.9 + - @trigger.dev/integration-kit@2.1.9 + +## 2.1.8 + +### Patch Changes + +- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support. +- 81e886a1: Fix `getAll` helper and search function params +- Updated dependencies [6a992a19] +- Updated dependencies [ab9e4a98] +- Updated dependencies [ab9e4a98] + - @trigger.dev/sdk@2.1.8 + - @trigger.dev/integration-kit@2.1.8 + +## 2.1.7 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.7 +- @trigger.dev/sdk@2.1.7 + +## 2.1.6 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.6 +- @trigger.dev/sdk@2.1.6 + +## 2.1.5 + +### Patch Changes + +- @trigger.dev/integration-kit@2.1.5 +- @trigger.dev/sdk@2.1.5 + +## 2.1.4 + +### Patch Changes + +- 15f17d27: First release of `@trigger.dev/linear` integration. `io.runTask()` error handlers can now prevent further retries. +- Updated dependencies [ad14983e] +- Updated dependencies [15f17d27] +- Updated dependencies [50137a6f] +- Updated dependencies [c0dfa804] + - @trigger.dev/sdk@2.1.4 + - @trigger.dev/integration-kit@2.1.4 diff --git a/integrations/linear/README.md b/integrations/linear/README.md new file mode 100644 index 0000000000..13ffab69b4 --- /dev/null +++ b/integrations/linear/README.md @@ -0,0 +1,3 @@ + +# @trigger.dev/linear + \ No newline at end of file diff --git a/integrations/linear/package.json b/integrations/linear/package.json new file mode 100644 index 0000000000..94c63d85a5 --- /dev/null +++ b/integrations/linear/package.json @@ -0,0 +1,37 @@ +{ + "name": "@trigger.dev/linear", + "version": "2.1.9", + "description": "Trigger.dev integration for @linear/sdk", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/index.js", + "dist/index.d.ts", + "dist/index.js.map" + ], + "devDependencies": { + "@trigger.dev/tsconfig": "workspace:*", + "@types/node": "16.x", + "rimraf": "^3.0.2", + "tsup": "7.1.x", + "typescript": "4.9.4" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@linear/sdk": "^8.0.0", + "@trigger.dev/integration-kit": "workspace:^2.1.9", + "@trigger.dev/sdk": "workspace:^2.1.9", + "zod": "3.22.3" + }, + "engines": { + "node": ">=16.8.0" + } +} diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts new file mode 100644 index 0000000000..18dff27b58 --- /dev/null +++ b/integrations/linear/src/events.ts @@ -0,0 +1,535 @@ +import { EventSpecification } from "@trigger.dev/sdk"; +import { + AttachmentEvent, + CommentEvent, + CycleEvent, + IssueEvent, + IssueLabelEvent, + IssueSLAEvent, + ProjectEvent, + ProjectUpdateEvent, + ReactionEvent, +} from "./schemas"; +import { GetLinearPayload } from "./types"; +import { + attachmentCreated, + attachmentRemoved, + attachmentUpdated, + commentCreated, + commentRemoved, + commentUpdated, + cycleCreated, + cycleRemoved, + cycleUpdated, + issueCreated, + issueRemoved, + issueUpdated, + issueLabelCreated, + issueLabelRemoved, + issueLabelUpdated, + projectCreated, + projectRemoved, + projectUpdated, + projectUpdateCreated, + projectUpdateRemoved, + projectUpdateUpdated, + reactionCreated, + reactionRemoved, + reactionUpdated, +} from "./payload-examples"; +import { onCommentProperties, onIssueProperties, updatedFromProperties } from "./utils"; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachment: EventSpecification> = { + name: "Attachment", + title: "On Attachment", + source: "linear.app", + icon: "linear", + examples: [attachmentCreated, attachmentRemoved, attachmentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Attachment ID", text: payload.data.id }, + ], +}; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachmentCreated: EventSpecification> = + { + name: "Attachment", + title: "On Attachment Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [attachmentCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], + }; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachmentRemoved: EventSpecification> = + { + name: "Attachment", + title: "On Attachment Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [attachmentRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], + }; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachmentUpdated: EventSpecification> = + { + name: "Attachment", + title: "On Attachment Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [attachmentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], + }; + +export const onComment: EventSpecification> = { + name: "Comment", + title: "On Comment", + source: "linear.app", + icon: "linear", + examples: [commentCreated, commentRemoved, commentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + ...onCommentProperties(payload), + ...updatedFromProperties(payload), + ], +}; + +export const onCommentCreated: EventSpecification> = { + name: "Comment", + title: "On Comment Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [commentCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onCommentProperties(payload), +}; + +export const onCommentRemoved: EventSpecification> = { + name: "Comment", + title: "On Comment Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [commentRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onCommentProperties(payload), +}; + +export const onCommentUpdated: EventSpecification> = { + name: "Comment", + title: "On Comment Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [commentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)], +}; + +export const onCycle: EventSpecification> = { + name: "Cycle", + title: "On Cycle", + source: "linear.app", + icon: "linear", + examples: [cycleCreated, cycleRemoved, cycleUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Cycle ID", text: payload.data.id }, + ], +}; + +export const onCycleCreated: EventSpecification> = { + name: "Cycle", + title: "On Cycle Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [cycleCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], +}; + +export const onCycleRemoved: EventSpecification> = { + name: "Cycle", + title: "On Cycle Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [cycleRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], +}; + +export const onCycleUpdated: EventSpecification> = { + name: "Cycle", + title: "On Cycle Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [cycleUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], +}; + +export const onIssue: EventSpecification> = { + name: "Issue", + title: "On Issue", + source: "linear.app", + icon: "linear", + examples: [issueCreated, issueRemoved, issueUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + ...onIssueProperties(payload), + ...updatedFromProperties(payload), + ], +}; + +export const onIssueCreated: EventSpecification> = { + name: "Issue", + title: "On Issue Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [issueCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onIssueProperties(payload), +}; + +export const onIssueRemoved: EventSpecification> = { + name: "Issue", + title: "On Issue Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [issueRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onIssueProperties(payload), +}; + +export const onIssueUpdated: EventSpecification> = { + name: "Issue", + title: "On Issue Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [issueUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)], +}; + +export const onIssueLabel: EventSpecification> = { + name: "IssueLabel", + title: "On IssueLabel", + source: "linear.app", + icon: "linear", + examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "IssueLabel ID", text: payload.data.id }, + ], +}; + +export const onIssueLabelCreated: EventSpecification> = + { + name: "IssueLabel", + title: "On IssueLabel Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [issueLabelCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], + }; + +export const onIssueLabelRemoved: EventSpecification> = + { + name: "IssueLabel", + title: "On IssueLabel Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [issueLabelRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], + }; + +export const onIssueLabelUpdated: EventSpecification> = + { + name: "IssueLabel", + title: "On IssueLabel Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [issueLabelUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], + }; + +// TODO: this needs to be tested +export const onIssueSLA: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "SLA action", text: payload.action }, + { label: "Issue ID", text: payload.issueData.id }, + ], +}; + +export const onIssueSLASet: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA Set", + source: "linear.app", + icon: "linear", + filter: { + action: ["set"], + }, + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }], +}; + +export const onIssueSLABreached: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA Breached", + source: "linear.app", + icon: "linear", + filter: { + action: ["breached"], + }, + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }], +}; + +export const onIssueSLAHighRisk: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA High Risk", + source: "linear.app", + icon: "linear", + filter: { + action: ["highRisk"], + }, + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }], +}; + +export const onProject: EventSpecification> = { + name: "Project", + title: "On Project", + source: "linear.app", + icon: "linear", + examples: [projectCreated, projectRemoved, projectUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ...updatedFromProperties(payload), + ], +}; + +export const onProjectCreated: EventSpecification> = { + name: "Project", + title: "On Project Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [projectCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ], +}; + +export const onProjectRemoved: EventSpecification> = { + name: "Project", + title: "On Project Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [projectRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ], +}; + +export const onProjectUpdated: EventSpecification> = { + name: "Project", + title: "On Project Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [projectUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ...updatedFromProperties(payload), + ], +}; + +export const onProjectUpdate: EventSpecification> = { + name: "ProjectUpdate", + title: "On ProjectUpdate", + source: "linear.app", + icon: "linear", + examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "ProjectUpdate ID", text: payload.data.id }, + ], +}; + +export const onProjectUpdateCreated: EventSpecification< + GetLinearPayload +> = { + name: "ProjectUpdate", + title: "On ProjectUpdate Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [projectUpdateCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], +}; + +export const onProjectUpdateRemoved: EventSpecification< + GetLinearPayload +> = { + name: "ProjectUpdate", + title: "On ProjectUpdate Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [projectUpdateRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], +}; + +export const onProjectUpdateUpdated: EventSpecification< + GetLinearPayload +> = { + name: "ProjectUpdate", + title: "On ProjectUpdate Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [projectUpdateUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], +}; + +export const onReaction: EventSpecification> = { + name: "Reaction", + title: "On Reaction", + source: "linear.app", + icon: "linear", + examples: [reactionCreated, reactionRemoved, reactionUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Reaction ID", text: payload.data.id }, + ], +}; + +export const onReactionCreated: EventSpecification> = { + name: "Reaction", + title: "On Reaction Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [reactionCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], +}; + +export const onReactionRemoved: EventSpecification> = { + name: "Reaction", + title: "On Reaction Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [reactionRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], +}; + +export const onReactionUpdated: EventSpecification> = { + name: "Reaction", + title: "On Reaction Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [reactionUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], +}; diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts new file mode 100644 index 0000000000..0f53f9e7ac --- /dev/null +++ b/integrations/linear/src/index.ts @@ -0,0 +1,2029 @@ +import { + ConnectionAuth, + IO, + IOTask, + IntegrationTaskKey, + Json, + Prettify, + RunTaskErrorCallback, + RunTaskOptions, + TriggerIntegration, + retry, +} from "@trigger.dev/sdk"; +import { + Attachment, + AttachmentConnection, + AttachmentPayload, + Comment, + CommentConnection, + CommentPayload, + Connection, + CreateOrJoinOrganizationResponse, + CycleArchivePayload, + CyclePayload, + DeletePayload, + Document, + DocumentConnection, + DocumentPayload, + DocumentSearchPayload, + Favorite, + FavoriteConnection, + FavoritePayload, + FrontAttachmentPayload, + Issue, + IssueArchivePayload, + IssueConnection, + IssueLabel, + IssueLabelConnection, + IssueLabelPayload, + IssuePayload, + IssuePriorityValue, + IssueRelation, + IssueRelationConnection, + IssueRelationPayload, + IssueSearchPayload, + LinearClient, + LinearDocument as L, + LinearError, + Notification, + NotificationArchivePayload, + NotificationConnection, + NotificationSubscriptionPayload, + Organization, + OrganizationInvitePayload, + Project, + ProjectArchivePayload, + ProjectConnection, + ProjectLink, + ProjectLinkConnection, + ProjectLinkPayload, + ProjectMilestonePayload, + ProjectPayload, + ProjectSearchPayload, + ProjectUpdate, + ProjectUpdateConnection, + ProjectUpdatePayload, + RatelimitedLinearError, + ReactionPayload, + RoadmapArchivePayload, + RoadmapPayload, + Team, + TeamConnection, + TeamMembership, + TeamMembershipConnection, + TeamMembershipPayload, + TeamPayload, + Template, + User, + UserConnection, + UserPayload, + WorkflowState, + WorkflowStateArchivePayload, + WorkflowStateConnection, + WorkflowStatePayload, +} from "@linear/sdk"; + +import * as events from "./events"; +import { AwaitNested, LinearReturnType, SerializedLinearOutput } from "./types"; +import { Nullable, QueryVariables, queryProperties } from "./utils"; +import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; + +export type LinearIntegrationOptions = { + id: string; + apiKey?: string; +}; + +export type LinearRunTask = InstanceType["runTask"]; + +export class Linear implements TriggerIntegration { + private _options: LinearIntegrationOptions; + private _client?: LinearClient; + private _io?: IO; + private _connectionKey?: string; + + constructor(private options: LinearIntegrationOptions) { + if (Object.keys(options).includes("apiKey") && !options.apiKey) { + throw `Can't create Linear integration (${options.id}) as apiKey was undefined`; + } + + this._options = options; + } + + get authSource() { + return this._options.apiKey ? "LOCAL" : "HOSTED"; + } + + get id() { + return this._options.id; + } + + get metadata() { + return { id: "linear", name: "Linear" }; + } + + get source() { + return createWebhookEventSource(this); + } + + cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { + const linear = new Linear(this._options); + linear._io = io; + linear._connectionKey = connectionKey; + linear._client = this.createClient(auth); + return linear; + } + + createClient(auth?: ConnectionAuth) { + if (auth) { + return new LinearClient({ + accessToken: auth.accessToken, + }); + } + + if (this._options.apiKey) { + return new LinearClient({ + apiKey: this._options.apiKey, + }); + } + + throw new Error("No auth"); + } + + runTask | void>( + key: IntegrationTaskKey, + callback: (client: LinearClient, task: IOTask, io: IO) => Promise, + options?: RunTaskOptions, + errorCallback?: RunTaskErrorCallback + ): Promise { + if (!this._io) throw new Error("No IO"); + if (!this._connectionKey) throw new Error("No connection key"); + + return this._io.runTask( + key, + (task, io) => { + if (!this._client) throw new Error("No client"); + return callback(this._client, task, io); + }, + { + icon: "linear", + retry: retry.standardBackoff, + ...(options ?? {}), + connectionKey: this._connectionKey, + }, + errorCallback ?? onError + ); + } + + async getAll< + TTask extends ( + key: IntegrationTaskKey, + params: Partial> + ) => LinearReturnType>, + >( + task: TTask, + key: IntegrationTaskKey, + params: Parameters[1] = {} + ): Promise>["nodes"]> { + const boundTask = task.bind(this as any); + + let edges = await boundTask(`${key}-0`, params); + let nodes = edges.nodes; + + for (let i = 1; edges.pageInfo.hasNextPage; i++) { + edges = await boundTask(`${key}-${i}`, { ...params, after: edges.pageInfo.endCursor }); + nodes = nodes.concat(edges.nodes); + } + + return nodes; + } + + attachment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.attachment(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + } + ); + } + + attachments( + key: IntegrationTaskKey, + params: L.AttachmentsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.attachments(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Attachments", + params, + properties: queryProperties(params), + } + ); + } + + createAttachment( + key: IntegrationTaskKey, + params: L.AttachmentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createAttachment(params); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Create Attachment", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Title", text: params.title }, + { label: "URL", text: params.url }, + ], + } + ); + } + + deleteAttachment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteAttachment(params.id), { + name: "Delete Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + }); + } + + updateAttachment( + key: IntegrationTaskKey, + params: { id: string; input: L.AttachmentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateAttachment(params.id, params.input); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Update Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + } + ); + } + + attachmentLinkDiscord( + key: IntegrationTaskKey, + params: { + channelId: string; + issueId: string; + messageId: string; + url: string; + variables?: Omit< + L.AttachmentLinkDiscordMutationVariables, + "channelId" | "issueId" | "messageId" | "url" + >; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkDiscord( + params.channelId, + params.issueId, + params.messageId, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Discord Message", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Channel ID", text: params.channelId }, + { label: "Message ID", text: params.messageId }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkFront( + key: IntegrationTaskKey, + params: { + conversationId: string; + issueId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkFront( + params.conversationId, + params.issueId, + params.variables + ); + return serializeLinearOutput(payload); + }, + { + name: "Link Front Conversation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Conversation ID", text: params.conversationId }, + ], + } + ); + } + + attachmentLinkIntercom( + key: IntegrationTaskKey, + params: { + conversationId: string; + issueId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkIntercom( + params.conversationId, + params.issueId, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Intercom Conversation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Conversation ID", text: params.conversationId }, + ], + } + ); + } + + attachmentLinkJiraIssue( + key: IntegrationTaskKey, + params: { + issueId: string; + jiraIssueId: string; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkJiraIssue(params.issueId, params.jiraIssueId); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Jira Issue", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Jira Issue ID", text: params.jiraIssueId }, + ], + } + ); + } + + attachmentLinkSlack( + key: IntegrationTaskKey, + params: { + channel: string; + issueId: string; + latest: string; + url: string; + variables?: Omit< + L.AttachmentLinkSlackMutationVariables, + "channel" | "issueId" | "latest" | "url" + >; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkSlack( + params.channel, + params.issueId, + params.latest, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Slack Message", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Channel", text: params.channel }, + { label: "Latest", text: params.latest }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkURL( + key: IntegrationTaskKey, + params: { + issueId: string; + url: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkURL( + params.issueId, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link URL", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkZendesk( + key: IntegrationTaskKey, + params: { + issueId: string; + ticketId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkZendesk( + params.issueId, + params.ticketId, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Zendesk Ticket", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Ticket ID", text: params.ticketId }, + ], + } + ); + } + + comment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.comment(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + comments( + key: IntegrationTaskKey, + params: L.CommentsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.comments(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Comments", + params, + properties: queryProperties(params), + } + ); + } + + createComment( + key: IntegrationTaskKey, + params: L.CommentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createComment(params); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Create Comment", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Body", text: params.body ?? "" }, + ], + } + ); + } + + deleteComment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteComment(params.id), { + name: "Delete Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + }); + } + + updateComment( + key: IntegrationTaskKey, + params: { id: string; input: L.CommentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateComment(params.id, params.input); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Update Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + archiveCycle( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveCycle(params.id); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + + createCycle( + key: IntegrationTaskKey, + params: L.CycleCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createCycle(params); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Create Cycle", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Start at", text: params.startsAt.toISOString() }, + { label: "Ends at", text: params.endsAt.toISOString() }, + ], + } + ); + } + + // deleteCycle() does not exist + + updateCycle( + key: IntegrationTaskKey, + params: { id: string; input: L.CycleUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateCycle(params.id, params.input); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Update Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + + document(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.document(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Document", + params, + properties: [{ label: "Document ID", text: params.id }], + } + ); + } + + documents( + key: IntegrationTaskKey, + params: L.DocumentsQueryVariables + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.documents(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Documents", + params, + properties: queryProperties(params), + } + ); + } + + createDocument( + key: IntegrationTaskKey, + params: L.DocumentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createDocument(params); + return serializeLinearOutput(await payload.document); + }, + { + name: "Create Document", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + searchDocuments( + key: IntegrationTaskKey, + params: { + term: string; + variables?: Parameters[1]; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchDocuments(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Documents", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + favorite(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.favorite(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Favorite", + params, + properties: [{ label: "Favorite ID", text: params.id }], + } + ); + } + + favorites( + key: IntegrationTaskKey, + params: L.FavoritesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.favorites(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Favorites", + params, + properties: queryProperties(params), + } + ); + } + + createFavorite( + key: IntegrationTaskKey, + params: L.FavoriteCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createFavorite(params); + return serializeLinearOutput(await payload.favorite); + }, + { + name: "Create Favorite", + params, + } + ); + } + + issue(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issue(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issues( + key: IntegrationTaskKey, + params: L.IssuesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issues(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Issues", + params, + properties: queryProperties(params), + } + ); + } + + archiveIssue( + key: IntegrationTaskKey, + params: { + id: string; + variables?: Omit; + } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveIssue(params.id, params.variables); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + createIssue( + key: IntegrationTaskKey, + params: L.IssueCreateInput & { title: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssue(params); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Create Issue", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + deleteIssue( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteIssue(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + searchIssues( + key: IntegrationTaskKey, + params: { + term: string; + variables?: Parameters[1]; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchIssues(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Issues", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + updateIssue( + key: IntegrationTaskKey, + params: { id: string; input: L.IssueUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssue(params.id, params.input); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Update Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issueLabel(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueLabel(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueLabel", + params, + properties: [{ label: "IssueLabel ID", text: params.id }], + } + ); + } + + issueLabels( + key: IntegrationTaskKey, + params: L.IssueLabelsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueLabels(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueLabels", + params, + properties: queryProperties(params), + } + ); + } + + createIssueLabel( + key: IntegrationTaskKey, + params: L.IssueLabelCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueLabel(params); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Create IssueLabel", + params, + properties: [{ label: "Label name", text: params.name }], + } + ); + } + + deleteIssueLabel(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteIssueLabel(params.id), { + name: "Delete IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + }); + } + + updateIssueLabel( + key: IntegrationTaskKey, + params: { id: string; input: L.IssueLabelUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssueLabel(params.id, params.input); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Update IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + } + ); + } + + issuePriorityValues(key: IntegrationTaskKey): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issuePriorityValues; + return serializeLinearOutput(entity); + }, + { + name: "Get Issue Priority Values", + } + ); + } + + issueRelation(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueRelation(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueRelation", + params, + properties: [{ label: "IssueRelation ID", text: params.id }], + } + ); + } + + issueRelations( + key: IntegrationTaskKey, + params: L.IssueRelationsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueRelations(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueRelations", + params, + properties: queryProperties(params), + } + ); + } + + createIssueRelation( + key: IntegrationTaskKey, + params: L.IssueRelationCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueRelation(params); + return serializeLinearOutput(await payload.issueRelation); + }, + { + name: "Create IssueRelation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Related Issue ID", text: params.relatedIssueId }, + { label: "Relation Type", text: params.type }, + ], + } + ); + } + + notification(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.notification(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Notification", + params, + properties: [{ label: "Notification ID", text: params.id }], + } + ); + } + + notifications( + key: IntegrationTaskKey, + params: L.NotificationsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.notifications(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Notifications", + params, + properties: queryProperties(params), + } + ); + } + + archiveNotification( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveNotification(params.id); + return serializeLinearOutput(payload); + }, + { + name: "Archive Notification", + params, + properties: [{ label: "Notification ID", text: params.id }], + } + ); + } + + createNotificationSubscription( + key: IntegrationTaskKey, + params: { + input: L.NotificationSubscriptionCreateInput; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createNotificationSubscription(params.input); + return serializeLinearOutput(payload); + }, + { + name: "Create Notification Subscription", + params, + } + ); + } + + organization(key: IntegrationTaskKey): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.organization; + return serializeLinearOutput(entity); + }, + { + name: "Get Viewer's Organization", + } + ); + } + + createOrganizationFromOnboarding( + key: IntegrationTaskKey, + params: { + input: L.CreateOrganizationInput; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createOrganizationFromOnboarding( + params.input, + params.variables + ); + return serializeLinearOutput(await payload.organization); + }, + { + name: "Create Organization", + params, + properties: [ + { label: "Name", text: params.input.name }, + { label: "URL Key", text: params.input.urlKey }, + ], + } + ); + } + + /** WARNING: Causes internal server errors on Linear's side, regardless of input. */ + createOrganizationInvite( + key: IntegrationTaskKey, + params: { + input: L.OrganizationInviteCreateInput; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createOrganizationInvite(params.input); + return serializeLinearOutput(await payload.organizationInvite); + }, + { + name: "Create Organization Invite", + params, + properties: [{ label: "Invitee Email", text: params.input.email }], + } + ); + } + + project(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.project(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projects( + key: IntegrationTaskKey, + params: L.ProjectsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projects(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Projects", + params, + properties: queryProperties(params), + } + ); + } + + archiveProject( + key: IntegrationTaskKey, + params: { + id: string; + variables?: Omit; + } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveProject(params.id, params.variables); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + createProject( + key: IntegrationTaskKey, + params: L.ProjectCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProject(params); + return serializeLinearOutput(await payload.project); + }, + { + name: "Create Project", + params, + properties: [ + { label: "Team IDs", text: params.teamIds.join(", ") }, + { label: "Project name", text: params.name }, + ], + } + ); + } + + deleteProject( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteProject(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + searchProjects( + key: IntegrationTaskKey, + params: { + term: string; + variables?: Parameters[1]; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchProjects(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Projects", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + updateProject( + key: IntegrationTaskKey, + params: { id: string; input: L.ProjectUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProject(params.id, params.input); + return serializeLinearOutput(await payload.project); + }, + { + name: "Update Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projectLink(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectLink(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectLink", + params, + properties: [{ label: "ProjectLink ID", text: params.id }], + } + ); + } + + projectLinks( + key: IntegrationTaskKey, + params: L.ProjectLinksQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projectLinks(params); + return serializeLinearOutput(edges); + }, + { + name: "Get ProjectLinks", + params, + properties: queryProperties(params), + } + ); + } + + createProjectLink( + key: IntegrationTaskKey, + params: L.ProjectLinkCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectLink(params); + return serializeLinearOutput(await payload.projectLink); + }, + { + name: "Create ProjectLink", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Link Label", text: params.label }, + { label: "Link URL", text: params.url }, + ], + } + ); + } + + createProjectMilestone( + key: IntegrationTaskKey, + params: L.ProjectMilestoneCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectMilestone(params); + return serializeLinearOutput(await payload.projectMilestone); + }, + { + name: "Create ProjectMilestone", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Milestone Name", text: params.name }, + ], + } + ); + } + + projectUpdate(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectUpdate(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + projectUpdates( + key: IntegrationTaskKey, + params: L.ProjectUpdatesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projectUpdates(params); + return serializeLinearOutput(edges); + }, + { + name: "Get ProjectUpdates", + params, + properties: queryProperties(params), + } + ); + } + + createProjectUpdate( + key: IntegrationTaskKey, + params: L.ProjectUpdateCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectUpdate(params); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Create ProjectUpdate", + params, + properties: [{ label: "Project ID", text: params.projectId }], + } + ); + } + + deleteProjectUpdate(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteProjectUpdate(params.id), { + name: "Delete ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + }); + } + + updateProjectUpdate( + key: IntegrationTaskKey, + params: { id: string; input: L.ProjectUpdateUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProjectUpdate(params.id, params.input); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Update ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + createReaction( + key: IntegrationTaskKey, + params: L.ReactionCreateInput & { emoji: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createReaction(params); + return serializeLinearOutput(await payload.reaction); + }, + { + name: "Create Reaction", + params, + properties: [ + ...(params.commentId ? [{ label: "Comment ID", text: params.commentId }] : []), + ...(params.issueId ? [{ label: "Issue ID", text: params.issueId }] : []), + ...(params.projectUpdateId + ? [{ label: "ProjectUpdate ID", text: params.projectUpdateId }] + : []), + { label: "Emoji", text: params.emoji }, + ], + } + ); + } + + deleteReaction(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteReaction(params.id), { + name: "Delete Reaction", + params, + properties: [{ label: "Reaction ID", text: params.id }], + }); + } + + archiveRoadmap( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveRoadmap(params.id); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Roadmap", + params, + properties: [{ label: "Roadmap ID", text: params.id }], + } + ); + } + + createRoadmap( + key: IntegrationTaskKey, + params: L.RoadmapCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createRoadmap(params); + return serializeLinearOutput(await payload.roadmap); + }, + { + name: "Create Roadmap", + params, + properties: [{ label: "Roadmap Name", text: params.name }], + } + ); + } + + team(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.team(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Team", + params, + properties: [{ label: "Team ID", text: params.id }], + } + ); + } + + teams( + key: IntegrationTaskKey, + params: L.TeamsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.teams(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Teams", + params, + properties: queryProperties(params), + } + ); + } + + createTeam( + key: IntegrationTaskKey, + params: L.TeamCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createTeam(params); + return serializeLinearOutput(await payload.team); + }, + { + name: "Create Team", + params, + properties: [{ label: "Team Name", text: params.name }], + } + ); + } + + teamMembership( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.teamMembership(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get TeamMembership", + params, + properties: [{ label: "TeamMembership ID", text: params.id }], + } + ); + } + + teamMemberships( + key: IntegrationTaskKey, + params: L.TeamMembershipsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.teamMemberships(params); + return serializeLinearOutput(edges); + }, + { + name: "Get TeamMemberships", + params, + properties: queryProperties(params), + } + ); + } + + createTeamMembership( + key: IntegrationTaskKey, + params: L.TeamMembershipCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createTeamMembership(params); + return serializeLinearOutput(await payload.teamMembership); + }, + { + name: "Create TeamMembership", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "User ID", text: params.userId }, + ], + } + ); + } + + template(key: IntegrationTaskKey, params: { id: string }): LinearReturnType