Compare commits

...

55 Commits

Author SHA1 Message Date
Eric Allam 321378db4f Run pnpm i to fix lock file
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-09 12:40:48 +01:00
github-actions[bot] c8b10ce188 chore: Update version for release (#291)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-09 12:40:15 +01:00
Eric Allam 57ed2b2d0d Fixed cli type errors 2023-08-09 12:37:22 +01:00
Eric Allam fa3a22eb78 Added an $isNull EventFilter condition matcher 2023-08-09 12:35:47 +01:00
Dan a1bc15d18b Updated the example job (#288)
* Changed the example job to include a delay

* Improved the example job copy and added ‘in a separate terminal’ to 2.

* Create kind-eggs-pump.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-09 12:21:46 +01:00
Yaswanth 3f48dd0a68 [TRI-945]: Adds manual project setup guide to the docs (#286)
Co-authored-by: Crossover <crossover@MacBook-Pro.local>
2023-08-09 12:18:05 +01:00
Eric Allam 5c71868a73 Fix the examples in the supabase integration 2023-08-09 12:16:27 +01:00
Chigala 3ce6decef4 Feat/scaffold job catalog file (#283)
* feat: adds a job catalog file when an integration is created via the CLI

* added a changeset

* fix: create-integration CLI throws an error when it tries to install dependencies

* Update getUserPkgManager.ts to main

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-09 10:42:20 +01:00
Eric Allam ab007f2708 Fix the CLI typecheck error 2023-08-09 10:41:06 +01:00
Eric Allam 8412680863 Adding createImageEdit and createImageVariation tasks to openai 2023-08-09 10:39:17 +01:00
Matt Aitken a90908df6e CLI init fixes: don't ask for endpoint slug, fix for next.config and package manager artifact issue (#287)
* If an endpointId isn’t in the arguments or package, then use the project slug. Don’t ask for it.

* Should await the package artifact detection

* Catch error thrown if there’s no next.config file when detecting the type of Next project

* Create long-carrots-camp.md
2023-08-09 10:37:10 +01:00
Eric Allam a82b86210f Fix pnpm lock file for package release 2023-08-08 22:08:40 +01:00
github-actions[bot] 0367a92731 chore: Update version for release (#281)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-08 22:08:12 +01:00
Eric Allam d230cf622f Treat @trigger.dev/sdk the same as @trigger.dev/core in the webapp
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-08 15:52:14 +01:00
Eric Allam a1bf073b43 Trigger another release run 2023-08-08 15:18:13 +01:00
Eric Allam 5588108b97 Add unit test running to the public CI 2023-08-08 15:04:15 +01:00
Eric Allam b9bcecffe3 Added some Supabase storage and auth triggers to the job catalog 2023-08-08 15:02:07 +01:00
Eric Allam 59075f5fc2 EventFilter now supports more complex condition filters #271 2023-08-08 15:02:07 +01:00
Sai Hari fa942fc9f5 feat: 🎸 add end-to-end test harness (#253)
* feat: 🎸 add end-to-end test harness

* Address PR comments
2023-08-08 13:55:25 +01:00
D-K-P 47a8a557cf Replaced all the screenshots of the environments page 2023-08-08 11:47:25 +01:00
Yaswanth b51d0b5399 Fix:[TRI-879]-Hides help panel when endpoints exists on the Environme… (#256)
* Fix:[TRI-879]-Hides help panel when endpoints exists on the Environments & API Keys page

* fix: adds check for fully configured client

g

---------

Co-authored-by: Crossover <crossover@MacBook-Pro.local>
2023-08-08 11:15:58 +01:00
D-K-P 763c84e3b3 Replaced api key screenshot and text 2023-08-08 10:41:33 +01:00
Eric Allam dfb00e84e9 Fixing pnpm lock file 2023-08-08 07:35:25 +01:00
github-actions[bot] b5a64545bb chore: Update version for release (#273)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-08 07:32:24 +01:00
Eric Allam 3cf3eaff48 @trigger.dev/supabase: You can now trigger on multiple database events in the same job 2023-08-07 17:27:49 +01:00
Matt Aitken 4ca758a3de job-catalog: added initial setup and CLI build step
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-07 15:07:58 +01:00
Matt Aitken da81537009 Update job-catalog instructions
`pnpm run trigger:dev` should be `pnpm run dev:trigger`
2023-08-07 14:57:08 +01:00
Eric Allam 7c1b13ba3b Increase webhook delivery attempts to the max 25 attempts over 3 days 2023-08-07 14:25:23 +01:00
D-K-P 4b654e5b3b Fixed broken docs link 2023-08-07 13:29:11 +01:00
Eric Allam 726a50ace7 Point to the workspace version of @trigger.dev/sdk 2023-08-07 13:17:36 +01:00
github-actions[bot] 9deffb67c4 chore: Update version for release (#267)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-07 13:08:27 +01:00
Matt Aitken ff04bf44ee CLI: detect package manager using artifacts if they exist (#261)
* Detect package manager from artifacts (e.g. package-lock.json) and fallback to process.env.npm_config_user_agent

* Added a changeset
2023-08-07 13:01:22 +01:00
Matt Aitken e740297829 Cli: improve Next.js project detection (#262)
* Detect presence of a “next” dependency

* Use a strict undefined check instead

* Changeset: Detect Next.js project by looking at dependencies, not next.config.js

* Read a package json file

* First check for next.config file, otherwise use next dependency

* Update changeset description
2023-08-07 13:01:03 +01:00
D-K-P a31705e198 Added text summarizer / github issue reminder jobs to the examples table 2023-08-07 12:58:43 +01:00
Eric Allam f0bdf53364 Auto index production endpoints every 10 minutes 2023-08-07 12:49:37 +01:00
Matt Aitken 9638499163 Add your first endpoint in the dashboard (#269)
* Created the sheet

* Started work on the resource route

* Created ValidateCreateEndpointService and the EndpointValidateApi (name TBC)

* The TriggerClient responds with the id

* Tidied some stuff up

* Alternative: EndpointApi doesn’t take endpointSlug. Instead just Ping() does

* Create cool-snakes-deny.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-07 11:28:34 +01:00
Matt Aitken 9104c252b7 Telemetry (new user) (#268)
* Removed old telemetry

* Started adding TriggerClient telemetry

* Added a log if the telemetry client is created, event renamed to “user.created”
2023-08-07 11:28:13 +01:00
Matt Aitken 72cf345602 Added “thumbsRating” to docs pages 2023-08-07 10:50:02 +01:00
jemiluv8 d5b8f8299d update cli init to create index file in examples folder (#225)
* update cli init to create index file in examples folder

* add patch changeset

* Create silly-baboons-join.md

* remove comment in router file
add comment in jobs index to instruct them to

* rename examplesIndex to examplesIndexFileName

* import jobs module in routeContent for createTriggerPageRoute just as we did for createTriggerAppRoute

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-06 16:42:32 +01:00
Matt Aitken dd22b88d25 Latest lockfile… 🙄 2023-08-03 22:52:46 +01:00
github-actions[bot] 62b9c5879c chore: Update version for release (#248) 2023-08-03 22:47:23 +01:00
Eric Allam 1b0973fbc1 Fixed error when configuring a new endpoint failed and fixed ping to now throw an error when parsing JSON
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-03 16:02:16 +01:00
Eric Allam fc78854ed4 @trigger.dev/cli: Don't require the TRIGGER_API_URL to be set (it has a default) 2023-08-03 14:49:46 +01:00
Eric Allam ed15bb0618 Another small supabase tweak 2023-08-03 13:09:56 +01:00
Eric Allam ced9034428 Another small supabase doc fix 2023-08-03 13:08:42 +01:00
Eric Allam 7ec329d5d6 Fixed broken image in supabase docs 2023-08-03 13:06:23 +01:00
Eric Allam 81180999de The dev command should use a POST request when doing the PING to the local server 2023-08-03 13:01:08 +01:00
Eric Allam feaa79ecf3 @trigger.dev/supabase: Automatically enable database webhooks when using triggers 2023-08-03 13:00:29 +01:00
Eric Allam d34dc0847c More OpenAI examples 2023-08-03 09:55:07 +01:00
Eric Allam 1b7c7520b4 new Job -> client.defineJob 2023-08-03 09:53:15 +01:00
Eric Allam 34d77e830c Incorporate improved OpenAI docs from old repo 2023-08-03 09:51:25 +01:00
Eric Allam 87c302bf26 Document how to enable supabase database webhooks 2023-08-03 09:39:58 +01:00
Eric Allam d462c901f5 A couple of quickstart tweaks 2023-08-02 17:16:39 +01:00
D-K-P 55c3d79cc8 Deleted the info box on the integration pages 2023-08-02 17:11:41 +01:00
Eric Allam ce901c92e4 Update @trigger.dev/supabase to the latest supabase-management-js 2023-08-02 17:01:02 +01:00
173 changed files with 6774 additions and 1045 deletions
+85 -1
View File
@@ -64,8 +64,92 @@ jobs:
- name: 🔎 Type check
run: pnpm run typecheck --filter webapp
unitTests:
name: Unit Tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Run Unit Tests
run: |
pnpm run test
e2e:
name: e2e Tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: |
# Setup environment variables
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
cp ./packages/database/.env.example ./packages/database/.env
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
# Execute tests
pnpm run docker
pnpm run db:migrate
pnpm run db:seed
pnpm run test:e2e
# Cleanup
pnpm run docker:stop
- name: Upload Playwright report
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
publish:
needs: [typecheck]
needs: [typecheck, unitTests, e2e]
runs-on: buildjet-4vcpu-ubuntu-2204
outputs:
version: ${{ steps.get_version.outputs.version }}
+4 -1
View File
@@ -48,4 +48,7 @@ apps/**/public/build
.sentryclirc
.buildt
**/tmp/
**/tmp/
/test-results/
/playwright-report/
/playwright/.cache/
+46
View File
@@ -169,6 +169,52 @@ pnpm exec trigger-cli dev
9. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
## Running end-to-end webapp tests
To run the end-to-end tests, follow the steps below:
1. Set up environment variables (copy example envs into the correct place)
```sh
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
cp ./packages/database/.env.example ./packages/database/.env
```
2. Set up dependencies
```sh
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
```
3. Set up the database
```sh
pnpm run docker
pnpm run db:migrate
pnpm run db:seed
```
4. Run the end-to-end tests
```sh
pnpm run test:e2e
```
### Cleanup
The end-to-end tests use a `setup` and `teardown` script to seed the database with test data. If the test runner doesn't exit cleanly, then the database can be left in a state where the tests can't run because the `setup` script will try to create data that already exists. If this happens, you can manually delete the `users` and `organizations` from the database using prisma studio:
```sh
# With the database running (i.e. pnpm run docker)
pnpm run db:studio
```
## Add sample jobs
The [examples/jobs-starter](./examples/jobs-starter/) project defines simple jobs you can get started with.
@@ -51,11 +51,6 @@ export function NoIntegrationSheet({
)}
</SheetHeader>
<SheetBody>
<Callout variant="info">
We dont have an Integration for the {api.name} API yet but you can request one by
clicking the button above. In the meantime, connect to {api.name} using one of the
methods below.
</Callout>
<CustomHelp name={api.name} />
</SheetBody>
</SheetContent>
+2
View File
@@ -20,6 +20,8 @@ const EnvironmentSchema = z.object({
.default(process.env.NODE_ENV),
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_URL: z.string().optional(),
HIGHLIGHT_PROJECT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
+99 -1
View File
@@ -1,4 +1,6 @@
import type {
CronItem,
CronItemOptions,
Job as GraphileJob,
Runner as GraphileRunner,
JobHelpers,
@@ -7,7 +9,7 @@ import type {
TaskList,
TaskSpec,
} from "graphile-worker";
import { run as graphileRun } from "graphile-worker";
import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
@@ -18,6 +20,13 @@ export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
}
const RawCronPayloadSchema = z.object({
_cron: z.object({
ts: z.coerce.date(),
backfilled: z.boolean(),
}),
});
const GraphileJobSchema = z.object({
id: z.coerce.string(),
queue_name: z.string().nullable(),
@@ -50,6 +59,19 @@ export type ZodTasks<TConsumerSchema extends MessageCatalogSchema> = {
};
};
type RecurringTaskPayload = {
ts: Date;
backfilled: boolean;
};
export type ZodRecurringTasks = {
[key: string]: {
pattern: string;
options?: CronItemOptions;
handler: (payload: RecurringTaskPayload, job: GraphileJob) => Promise<void>;
};
};
export type ZodWorkerEnqueueOptions = TaskSpec & {
tx?: PrismaClientOrTransaction;
};
@@ -59,6 +81,7 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
prisma: PrismaClient;
schema: TMessageCatalog;
tasks: ZodTasks<TMessageCatalog>;
recurringTasks?: ZodRecurringTasks;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
@@ -66,6 +89,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#prisma: PrismaClient;
#runnerOptions: RunnerOptions;
#tasks: ZodTasks<TMessageCatalog>;
#recurringTasks?: ZodRecurringTasks;
#runner?: GraphileRunner;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
@@ -73,6 +97,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#prisma = options.prisma;
this.#runnerOptions = options.runnerOptions;
this.#tasks = options.tasks;
this.#recurringTasks = options.recurringTasks;
}
public async initialize(): Promise<boolean> {
@@ -84,9 +109,12 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
runnerOptions: this.#runnerOptions,
});
const parsedCronItems = parseCronItems(this.#createCronItemsFromRecurringTasks());
this.#runner = await graphileRun({
...this.#runnerOptions,
taskList: this.#createTaskListFromTasks(),
parsedCronItems,
});
if (!this.#runner) {
@@ -192,9 +220,38 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
taskList[key] = task;
}
for (const [key] of Object.entries(this.#recurringTasks ?? {})) {
const task: Task = (payload, helpers) => {
return this.#handleRecurringTask(key, payload, helpers);
};
taskList[key] = task;
}
return taskList;
}
#createCronItemsFromRecurringTasks() {
const cronItems: CronItem[] = [];
if (!this.#recurringTasks) {
return cronItems;
}
for (const [key, task] of Object.entries(this.#recurringTasks)) {
const cronItem: CronItem = {
pattern: task.pattern,
identifier: key,
task: key,
options: task.options,
};
cronItems.push(cronItem);
}
return cronItems;
}
async #handleMessage<K extends keyof TMessageCatalog>(
typeName: K,
rawPayload: unknown,
@@ -226,4 +283,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
await task.handler(payload, job);
}
async #handleRecurringTask(
typeName: string,
rawPayload: unknown,
helpers: JobHelpers
): Promise<void> {
const job = helpers.job;
logger.debug("Received recurring task, calling handler", {
type: String(typeName),
payload: rawPayload,
job,
});
const recurringTask = this.#recurringTasks?.[typeName];
if (!recurringTask) {
throw new Error(`No recurring task for message type: ${String(typeName)}`);
}
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse recurring task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
try {
await recurringTask.handler(payload._cron, job);
} catch (error) {
logger.error("Failed to handle recurring task", {
error,
payload,
});
throw error;
}
}
}
@@ -0,0 +1,121 @@
import { conform, useForm, useInputEvent } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher } from "@remix-run/react";
import { InlineCode } from "~/components/code/InlineCode";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { FormError } from "~/components/primitives/FormError";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "~/components/primitives/Sheet";
import { TextLink } from "~/components/primitives/TextLink";
import { docsPath } from "~/utils/pathBuilder";
import { bodySchema } from "../resources.projects.$projectId.endpoint";
import { RuntimeEnvironment, RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { useRef, useState } from "react";
type FirstEndpointSheetProps = {
projectId: string;
environments: { id: string; type: RuntimeEnvironmentType }[];
};
export function FirstEndpointSheet({ projectId, environments }: FirstEndpointSheetProps) {
const setEndpointUrlFetcher = useFetcher();
const [form, { url, environmentId }] = useForm({
id: "new-endpoint-url",
lastSubmission: setEndpointUrlFetcher.data,
onValidate({ formData }) {
return parse(formData, { schema: bodySchema });
},
});
const loadingEndpointUrl = setEndpointUrlFetcher.state !== "idle";
return (
<Sheet>
<SheetTrigger>
<ButtonContent variant={"primary/medium"}>Add your first endpoint</ButtonContent>
</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
<div>
<Header1>Add your first endpoint</Header1>
<Paragraph variant="small">
We recommend you use{" "}
<TextLink href={docsPath("documentation/guides/cli")}>the CLI</TextLink> when working
in development.
</Paragraph>
</div>
</SheetHeader>
<SheetBody>
<setEndpointUrlFetcher.Form
method="post"
action={`/resources/projects/${projectId}/endpoint`}
{...form.props}
>
<InputGroup className="mb-4 max-w-none">
<Header2>Environment type</Header2>
<SelectGroup>
<Select name={"environmentId"} defaultValue={environments[0].id}>
<SelectTrigger size="secondary/small">
<SelectValue placeholder="Select environment" className="m-0 p-0" /> Environment
</SelectTrigger>
<SelectContent>
{environments.map((environment) => (
<SelectItem key={environment.id} value={environment.id}>
<EnvironmentLabel environment={environment} />
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<FormError id={environmentId.errorId}>{environmentId.error}</FormError>
</InputGroup>
<InputGroup className="max-w-none">
<Header2>Endpoint URL</Header2>
<div className="flex items-center">
<Input
className="rounded-r-none"
{...conform.input(url, { type: "url" })}
placeholder="URL for your Trigger API route"
/>
<Button
type="submit"
variant="primary/medium"
className="rounded-l-none"
disabled={loadingEndpointUrl}
LeadingIcon={loadingEndpointUrl ? "spinner-white" : undefined}
>
{loadingEndpointUrl ? "Saving" : "Save"}
</Button>
</div>
<FormError id={url.errorId}>{url.error}</FormError>
<FormError id={form.errorId}>{form.error}</FormError>
<Hint>
This is the URL of your Trigger API route, Typically this would be:{" "}
<InlineCode variant="extra-small">https://yourdomain.com/api/trigger</InlineCode>.
</Hint>
</InputGroup>
</setEndpointUrlFetcher.Form>
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -7,7 +7,7 @@ import { EnvironmentLabel, environmentTitle } from "~/components/environments/En
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { ButtonContent } from "~/components/primitives/Buttons";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
@@ -39,6 +39,7 @@ import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
import { Badge } from "~/components/primitives/Badge";
import { FirstEndpointSheet } from "./FirstEndpointSheet";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -95,6 +96,13 @@ export default function Page() {
};
}, [selected, clients]);
const isAnyClientFullyConfigured = useMemo(() => {
return clients.some((client) => {
const { DEVELOPMENT, PRODUCTION } = client.endpoints;
return PRODUCTION.state === "configured" && DEVELOPMENT.state === PRODUCTION.state;
});
}, [clients]);
const organization = useOrganization();
const project = useProject();
@@ -119,7 +127,7 @@ export default function Page() {
<PageDescription>API Keys and endpoints for your environments.</PageDescription>
</PageHeader>
<PageBody>
<Help defaultOpen>
<Help defaultOpen={!isAnyClientFullyConfigured}>
{(open) => (
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div>
@@ -202,7 +210,12 @@ export default function Page() {
</div>
))
) : (
<Paragraph>You have no clients yet</Paragraph>
<>
<Paragraph>Add your first endpoint</Paragraph>
<Paragraph>
<FirstEndpointSheet projectId={project.id} environments={environments} />
</Paragraph>
</>
)}
</div>
{selectedEndpoint && (
@@ -8,7 +8,7 @@ import { ProjectsMenu } from "~/components/navigation/ProjectsMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { projectPath } from "~/utils/pathBuilder";
@@ -33,7 +33,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
}
analytics.project.identify({ project });
telemetry.project.identify({ project });
return typedjson({
project,
@@ -6,7 +6,7 @@ import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { useOrganization } from "~/hooks/useOrganizations";
import { getOrganizationFromSlug } from "~/models/organization.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { commitCurrentOrgSession, setCurrentOrg } from "~/services/currentOrganization.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
@@ -25,7 +25,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
throw new Response("Not Found", { status: 404 });
}
analytics.organization.identify({ organization });
telemetry.organization.identify({ organization });
const session = await setCurrentOrg(organization.slug, request);
@@ -57,6 +57,12 @@ export async function action({ request, params }: ActionArgs) {
return json(submission);
}
return json(e, { status: 400 });
if (e instanceof Error) {
submission.error.url = `${e.name}: ${e.message}`;
} else {
submission.error.url = "Unknown error";
}
return json(submission, { status: 400 });
}
}
@@ -0,0 +1,71 @@
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 { 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);
const formData = await request.formData();
const submission = parse(formData, { schema: bodySchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
const environment = await prisma.runtimeEnvironment.findUnique({
include: {
organization: true,
project: true,
},
where: {
id: submission.value.environmentId,
},
});
if (!environment) {
submission.error.environmentId = "Environment not found";
return json(submission);
}
const service = new ValidateCreateEndpointService();
const result = await service.call({
url: submission.value.url,
environment,
});
return json(submission);
} catch (e) {
if (e instanceof CreateEndpointError) {
submission.error.url = e.message;
return json(submission);
}
if (e instanceof Error) {
submission.error.url = `${e.name}: ${e.message}`;
} else {
submission.error.url = "Unknown error";
}
return json(submission, { status: 400 });
}
}
@@ -1,364 +0,0 @@
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import type { User } from "~/models/user.server";
class BehaviouralAnalytics {
client: PostHog | undefined = undefined;
constructor(apiKey?: string) {
if (!apiKey) {
console.log("No PostHog API key, so analytics won't track");
return;
}
this.client = new PostHog(apiKey, { host: "https://app.posthog.com" });
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.client === undefined) return;
this.client.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
//todo Job
// workflow = {
// identify: ({ workflow }: { workflow: Workflow }) => {
// if (this.client === undefined) return;
// this.client.groupIdentify({
// groupType: "workflow",
// groupKey: workflow.id,
// properties: {
// name: workflow.title,
// slug: workflow.slug,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// });
// },
// new: ({
// userId,
// organizationId,
// workflow,
// workflowCount,
// }: {
// userId: string;
// organizationId: string;
// workflow: Workflow;
// workflowCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow created",
// organizationId: organizationId,
// jobId: workflow.id,
// eventProperties: {
// id: workflow.id,
// slug: workflow.slug,
// title: workflow.title,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// userProperties: {
// workflowCount: workflowCount,
// },
// });
// },
// };
// workflowRun = {
// new: ({
// userId,
// organizationId,
// workflowId,
// workflowRun,
// environmentType,
// runCount,
// }: {
// userId: string;
// organizationId: string;
// workflowId: string;
// workflowRun: WorkflowRun;
// environmentType: string;
// runCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow run created",
// eventProperties: {
// id: workflowRun.id,
// workflowId: workflowRun.workflowId,
// environmentId: workflowRun.environmentId,
// environmentType,
// eventRuleId: workflowRun.eventRuleId,
// eventId: workflowRun.eventId,
// error: workflowRun.error,
// status: workflowRun.status,
// attemptCount: workflowRun.attemptCount,
// createdAt: workflowRun.createdAt,
// updatedAt: workflowRun.updatedAt,
// startedAt: workflowRun.startedAt,
// finishedAt: workflowRun.finishedAt,
// timedOutAt: workflowRun.timedOutAt,
// timedOutReason: workflowRun.timedOutReason,
// isTest: workflowRun.isTest,
// },
// userProperties: {
// runCount: runCount,
// },
// organizationId: organizationId,
// jobId: workflowId,
// environmentId: workflowRun.environmentId,
// });
// },
// };
environment = {
identify: ({ environment }: { environment: RuntimeEnvironment }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "environment",
groupKey: environment.id,
properties: {
name: environment.slug,
slug: environment.slug,
organizationId: environment.organizationId,
createdAt: environment.createdAt,
updatedAt: environment.updatedAt,
},
});
},
};
telemetry = {
capture: ({
userId,
event,
properties,
organizationId,
environmentId,
}: {
userId: string;
event: string;
properties: Record<string | number, any>;
organizationId?: string;
environmentId?: string;
}) => {
this.#capture({
userId,
event,
eventProperties: properties,
organizationId,
environmentId,
});
},
};
#capture(event: CaptureEvent) {
if (this.client === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.client.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const analytics = new BehaviouralAnalytics(env.POSTHOG_PROJECT_KEY);
+6
View File
@@ -1,6 +1,7 @@
import type { DeliverEmail } from "emails";
import { EmailClient } from "emails";
import type { SendEmailOptions } from "remix-auth-email-link";
import { redirect } from "remix-typedjson";
import { env } from "~/env.server";
import type { User } from "~/models/user.server";
import type { AuthUser } from "./authUser";
@@ -14,6 +15,11 @@ const client = new EmailClient({
});
export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): Promise<void> {
// Auto redirect when in development mode
if (env.NODE_ENV === "development") {
throw redirect(options.magicLink);
}
return client.send({
email: "magic_link",
to: options.emailAddress,
+79 -14
View File
@@ -13,8 +13,10 @@ import {
RegisterTriggerBodySchema,
RunJobBody,
RunJobResponseSchema,
ValidateResponse,
ValidateResponseSchema,
} from "@trigger.dev/core";
import { safeBodyFromResponse } from "~/utils/json";
import { safeBodyFromResponse, safeParseBodyFromResponse } from "~/utils/json";
import { logger } from "./logger.server";
export class EndpointApiError extends Error {
@@ -25,20 +27,15 @@ export class EndpointApiError extends Error {
}
}
// TODO: this should work with tunnelling
export class EndpointApi {
constructor(
private apiKey: string,
private url: string,
private id: string
) {}
constructor(private apiKey: string, private url: string) {}
async ping(): Promise<PongResponse> {
async ping(endpointId: string): Promise<PongResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-endpoint-id": this.id,
"x-trigger-endpoint-id": endpointId,
"x-trigger-action": "PING",
},
});
@@ -73,13 +70,23 @@ export class EndpointApi {
};
}
const anyBody = await response.json();
const pongResponse = await safeParseBodyFromResponse(response, PongResponseSchema);
logger.debug("ping() response from endpoint", {
body: anyBody,
});
if (!pongResponse) {
return {
ok: false,
error: `Could not parse response from endpoint. Make sure it points to the correct URL (you might be missing /api/trigger)`,
};
}
return PongResponseSchema.parse(anyBody);
if (!pongResponse.success) {
return {
ok: false,
error: `Endpoint ${this.url} responded with error: ${pongResponse.error.message}`,
};
}
return pongResponse.data;
}
async indexEndpoint() {
@@ -265,6 +272,64 @@ export class EndpointApi {
return HttpSourceResponseSchema.parse(anyBody);
}
async validate(): Promise<ValidateResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-action": "VALIDATE",
},
});
if (!response) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}`,
};
}
if (response.status === 401) {
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
if (body) {
return {
ok: false,
error: body.message,
} as const;
}
return {
ok: false,
error: `Trigger API key is invalid`,
} as const;
}
if (!response.ok) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}. Status code: ${response.status}`,
};
}
const validateResponse = await safeParseBodyFromResponse(response, ValidateResponseSchema);
if (!validateResponse) {
return {
ok: false,
error: `Could not parse response from endpoint. Make sure it points to the correct URL (you might be missing /api/trigger)`,
};
}
if (!validateResponse.success) {
return {
ok: false,
error: `Endpoint ${this.url} responded with error: ${validateResponse.error.message}`,
};
}
return validateResponse.data;
}
}
async function safeFetch(url: string, options: RequestInit) {
@@ -34,9 +34,9 @@ export class CreateEndpointService {
}) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl, id);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const pong = await client.ping();
const pong = await client.ping(id);
if (!pong.ok) {
throw new CreateEndpointError("FAILED_PING", pong.error);
@@ -28,7 +28,7 @@ export class IndexEndpointService {
const endpoint = await findEndpoint(id);
// Make a request to the endpoint to fetch a list of jobs
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url, endpoint.slug);
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url);
const indexResponse = await client.indexEndpoint();
@@ -0,0 +1,44 @@
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
export class RecurringEndpointIndexService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(ts: Date) {
// Find all production endpoints that haven't been indexed in the last 10 minutes
const currentTimestamp = ts.getTime();
const endpoints = await this.#prismaClient.endpoint.findMany({
where: {
environment: {
type: RuntimeEnvironmentType.PRODUCTION,
},
indexings: {
none: {
createdAt: {
gt: new Date(currentTimestamp - 10 * 60 * 1000),
},
},
},
},
});
logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", {
count: endpoints.length,
});
// Enqueue each endpoint for indexing
for (const endpoint of endpoints) {
await workerQueue.enqueue("indexEndpoint", {
id: endpoint.id,
source: "INTERNAL",
});
}
}
}
@@ -0,0 +1,100 @@
import { customAlphabet } from "nanoid";
import { $transaction, prisma, PrismaClient } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { workerQueue } from "../worker.server";
import { CreateEndpointError } from "./createEndpoint.server";
import { EndpointApi } from "../endpointApi.server";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
export class ValidateCreateEndpointService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ environment, url }: { environment: AuthenticatedEnvironment; url: string }) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const validationResult = await client.validate();
if (!validationResult.ok) {
throw new Error(validationResult.error);
}
try {
const result = await $transaction(this.#prismaClient, async (tx) => {
const endpoint = await tx.endpoint.upsert({
where: {
environmentId_slug: {
environmentId: environment.id,
slug: validationResult.endpointId,
},
},
create: {
environment: {
connect: {
id: environment.id,
},
},
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
slug: validationResult.endpointId,
url: endpointUrl,
indexingHookIdentifier: indexingHookIdentifier(),
},
update: {
url: endpointUrl,
},
});
// Kick off process to fetch the jobs for this endpoint
await workerQueue.enqueue(
"indexEndpoint",
{
id: endpoint.id,
source: "INTERNAL",
},
{ tx }
);
return endpoint;
});
return result;
} catch (error) {
if (error instanceof Error) {
throw new CreateEndpointError("FAILED_UPSERT", error.message);
} else {
throw new CreateEndpointError("FAILED_UPSERT", "Something went wrong");
}
}
}
// If the endpoint URL points to localhost, and the RUNTIME_PLATFORM is docker-compose, then we need to rewrite the host to host.docker.internal
// otherwise we shouldn't change anything
#normalizeEndpointUrl(url: string) {
if (env.RUNTIME_PLATFORM === "docker-compose") {
const urlObj = new URL(url);
if (urlObj.hostname === "localhost") {
urlObj.hostname = "host.docker.internal";
return urlObj.toString();
}
}
return url;
}
}
@@ -1,6 +1,6 @@
import type { EventDispatcher, EventRecord } from "@trigger.dev/database";
import type { EventFilter } from "@trigger.dev/core";
import { EventFilterSchema } from "@trigger.dev/core";
import { EventFilterSchema, eventFilterMatches } from "@trigger.dev/core";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerQueue } from "../worker.server";
@@ -124,29 +124,6 @@ export class EventMatcher {
}
public matches(filter: EventFilter) {
return patternMatches(this.event, filter);
return eventFilterMatches(this.event, filter);
}
}
function patternMatches(payload: any, pattern: any): boolean {
for (const [patternKey, patternValue] of Object.entries(pattern)) {
const payloadValue = payload[patternKey];
if (Array.isArray(patternValue)) {
if (patternValue.length > 0 && !patternValue.includes(payloadValue)) {
return false;
}
} else if (typeof patternValue === "object") {
if (Array.isArray(payloadValue)) {
if (!payloadValue.some((item) => patternMatches(item, patternValue))) {
return false;
}
} else {
if (!patternMatches(payloadValue, patternValue)) {
return false;
}
}
}
}
return true;
}
@@ -10,11 +10,11 @@ const supabase = new SupabaseManagement({
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
id: "on-new-todos",
name: "On New Todos",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
trigger: supabase.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {
},
@@ -33,11 +33,11 @@ const supabase = new SupabaseManagement({
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
id: "on-new-todos",
name: "On New Todos",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
trigger: supabase.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {
},
+2 -2
View File
@@ -1,5 +1,5 @@
import type { User } from "~/models/user.server";
import { analytics } from "./analytics.server";
import { telemetry } from "./telemetry.server";
export async function postAuthentication({
user,
@@ -10,5 +10,5 @@ export async function postAuthentication({
loginMethod: User["authenticationMethod"];
isNewUser: boolean;
}) {
analytics.user.identify({ user, isNewUser });
telemetry.user.identify({ user, isNewUser });
}
@@ -54,7 +54,7 @@ export class PerformRunExecutionService {
async #executePreprocessing(execution: FoundRunExecution) {
const { run } = execution;
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url, run.endpoint.slug);
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const startedAt = new Date();
@@ -189,7 +189,7 @@ export class PerformRunExecutionService {
return;
}
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url, run.endpoint.slug);
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const startedAt = new Date();
@@ -55,8 +55,7 @@ export class DeliverHttpSourceRequestService {
const clientApi = new EndpointApi(
httpSourceRequest.environment.apiKey,
httpSourceRequest.endpoint.url,
httpSourceRequest.endpoint.slug
httpSourceRequest.endpoint.url
);
const { response, events } = await clientApi.deliverHttpSourceRequest({
@@ -0,0 +1,239 @@
import { TriggerClient } from "@trigger.dev/sdk";
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { User } from "~/models/user.server";
type Options = {
postHogApiKey?: string;
trigger?: {
apiKey: string;
apiUrl: string;
};
};
class Telemetry {
#posthogClient: PostHog | undefined = undefined;
#triggerClient: TriggerClient | undefined = undefined;
constructor({ postHogApiKey, trigger }: Options) {
if (postHogApiKey) {
this.#posthogClient = new PostHog(postHogApiKey, { host: "https://app.posthog.com" });
} else {
console.log("No PostHog API key, so analytics won't track");
}
if (trigger) {
this.#triggerClient = new TriggerClient({
id: "triggerdotdev",
apiKey: trigger.apiKey,
apiUrl: trigger.apiUrl,
});
console.log("Created telemetry TriggerClient");
}
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
this.#triggerClient?.sendEvent({
name: "user.created",
payload: {
userId: user.id,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.#posthogClient === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.#posthogClient === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
#capture(event: CaptureEvent) {
if (this.#posthogClient === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.#posthogClient.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const telemetry = new Telemetry({
postHogApiKey: env.POSTHOG_PROJECT_KEY,
trigger:
env.TELEMETRY_TRIGGER_API_KEY && env.TELEMETRY_TRIGGER_API_URL
? {
apiKey: env.TELEMETRY_TRIGGER_API_KEY,
apiUrl: env.TELEMETRY_TRIGGER_API_URL,
}
: undefined,
});
@@ -45,7 +45,7 @@ export class InitializeTriggerService {
},
});
const clientApi = new EndpointApi(environment.apiKey, endpoint.url, endpoint.slug);
const clientApi = new EndpointApi(environment.apiKey, endpoint.url);
const registerMetadata = await clientApi.initializeTrigger(dynamicTrigger.slug, payload.params);
+27 -1
View File
@@ -6,6 +6,7 @@ import { env } from "~/env.server";
import { ZodWorker } from "~/platform/zodWorker.server";
import { sendEmail } from "./email.server";
import { IndexEndpointService } from "./endpoints/indexEndpoint.server";
import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server";
import { DeliverEventService } from "./events/deliverEvent.server";
import { InvokeDispatcherService } from "./events/invokeDispatcher.server";
import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server";
@@ -95,6 +96,31 @@ function getWorkerQueue() {
pollInterval: 1000,
},
schema: workerCatalog,
recurringTasks: {
// Run this every 5 minutes
autoIndexProductionEndpoints: {
pattern: "*/5 * * * *",
handler: async (payload, job) => {
const service = new RecurringEndpointIndexService();
await service.call(payload.ts);
},
},
// Run this every hour
purgeOldIndexings: {
pattern: "0 * * * *",
handler: async (payload, job) => {
// Delete indexings that are older than 7 days
await prisma.endpointIndex.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
});
},
},
},
tasks: {
"events.invokeDispatcher": {
maxAttempts: 3,
@@ -154,7 +180,7 @@ function getWorkerQueue() {
},
},
deliverHttpSourceRequest: {
maxAttempts: 5,
maxAttempts: 25,
handler: async (payload, job) => {
const service = new DeliverHttpSourceRequestService();
+17
View File
@@ -43,3 +43,20 @@ export async function safeBodyFromResponse<T>(
return parsedJson.data;
}
}
export async function safeParseBodyFromResponse<T>(
response: Response,
schema: z.Schema<T>
): Promise<z.SafeParseReturnType<unknown, T> | undefined> {
try {
const unknownJson = await response.json();
if (!unknownJson) {
return;
}
const parsedJson = schema.safeParse(unknownJson);
return parsedJson;
} catch (error) {}
}
+2
View File
@@ -14,6 +14,7 @@
"start": "cross-env NODE_ENV=production node --max-old-space-size=8192 ./build/server.js",
"typecheck": "tsc --noEmit",
"db:seed": "node prisma/seed.js",
"db:seed:local": "ts-node prisma/seed.ts",
"generate:sourcemaps": "remix build --sourcemap",
"clean:sourcemaps": "run-s clean:sourcemaps:*",
"clean:sourcemaps:public": "rimraf ./build/**/*.map",
@@ -61,6 +62,7 @@
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@uiw/react-codemirror": "^4.19.5",
"class-variance-authority": "^0.5.2",
"clsx": "^1.2.1",
+13 -2
View File
@@ -12,8 +12,19 @@ module.exports = {
ignoredRouteFiles: ["**/.*"],
devServerPort: 8002,
serverModuleFormat: "cjs",
serverDependenciesToBundle: ["marked", "axios", "@trigger.dev/core", "emails", "highlight.run"],
serverDependenciesToBundle: [
"marked",
"axios",
"@trigger.dev/core",
"@trigger.dev/sdk",
"emails",
"highlight.run",
],
watchPaths: async () => {
return ["../../packages/core/src/**/*", "../../packages/emails/src/**/*"];
return [
"../../packages/core/src/**/*",
"../../packages/trigger-sdk/src/**/*",
"../../packages/emails/src/**/*",
];
},
};
+1 -1
View File
@@ -1,5 +1,5 @@
```typescript Wait example
new Job(client, {
client.defineJob({
id: "delay-job",
name: "Delay Job",
version: "0.0.1",
+1 -1
View File
@@ -1,5 +1,5 @@
```typescript
new Job(client, {
client.defineJob({
//... other options
integrations: {
slack,
+7 -21
View File
@@ -4,9 +4,8 @@ description: "Integrations make it easy to use APIs in your Jobs"
---
<Note>
You can use any API in your Jobs by using existing Node.js SDKs or HTTP
requests. Integrations just make it much easier especially when you want to
use OAuth. And you get great logging.
You can use any API in your Jobs by using existing Node.js SDKs or HTTP requests. Integrations
just make it much easier especially when you want to use OAuth. And you get great logging.
</Note>
An Integration is a package you install that makes it easy to work with a specific API. They:
@@ -35,7 +34,7 @@ const slack = new Slack({
id: "slack",
});
new Job(client, {
client.defineJob({
id: "alert-on-new-github-issues",
name: "Alert on new GitHub issues",
version: "0.1.1",
@@ -84,29 +83,16 @@ You can use OAuth to authenticate your internal team with an Integration or to a
## References
<CardGroup>
<Card
title="Integrations Dashboard"
icon="sidebar"
href="documentation/guides/integrations"
>
The Integrations Dashboard allows you to manage your Integrations and setup
OAuth.
<Card title="Integrations Dashboard" icon="sidebar" href="documentation/guides/integrations">
The Integrations Dashboard allows you to manage your Integrations and setup OAuth.
</Card>
<Card
title="Trigger.dev Connect"
icon="user-plus"
href="/documentation/concepts/connect"
>
<Card title="Trigger.dev Connect" icon="user-plus" href="/documentation/concepts/connect">
Authenticate your users with an Integration using Trigger.dev Connect.
</Card>
<Card title="View Integrations" icon="grid-2" href="/integrations">
Trigger.dev integrates with a wide range of services.
</Card>
<Card
title="Create an Integration"
icon="square-plus"
href="/integrations/create"
>
<Card title="Create an Integration" icon="square-plus" href="/integrations/create">
Create an Integration for your own use or as a public package.
</Card>
</CardGroup>
+2 -6
View File
@@ -17,7 +17,7 @@ A Job is made up of a few things:
```ts
//Job definition uses the client
new Job(client, {
client.defineJob({
// 1. Metadata
id: "event-1",
name: "Run when the foo.bar event happens",
@@ -51,11 +51,7 @@ Events [trigger](/documentation/concepts/triggers) Jobs. Jobs generate a [Run](/
<Card title="Job SDK reference" icon="wrench" href="/sdk/job">
Detailed SDK reference for Jobs.
</Card>
<Card
title="Managing Jobs Dashboard"
icon="globe"
href="/documentation/guides/managing-jobs"
>
<Card title="Managing Jobs Dashboard" icon="globe" href="/documentation/guides/managing-jobs">
Viewing and managing your Jobs in the Dashboard.
</Card>
</CardGroup>
+2 -6
View File
@@ -10,7 +10,7 @@ description: "When a [Job](/documentation/concepts/jobs) is [Triggered](/documen
A Run is a record of the execution of a Job. It is created from `run()` function of a Job.
```ts
new Job(client, {
client.defineJob({
id: "event-1",
name: "Run when the foo.bar event happens",
version: "0.0.1",
@@ -64,11 +64,7 @@ The `context` object gives you access to information about the current Run, Job,
## References
<CardGroup cols={2}>
<Card
title="Viewing Runs Dashboard"
icon="globe"
href="/documentation/guides/viewing-runs"
>
<Card title="Viewing Runs Dashboard" icon="globe" href="/documentation/guides/viewing-runs">
View all Runs for a Job, all the way down to individual Tasks.
</Card>
<Card title="`io` SDK Reference" icon="wrench" href="/sdk/io">
+7 -23
View File
@@ -10,7 +10,7 @@ description: "Tasks are individual building blocks of a Run."
In the `run()` function you can use regular code and you can use Tasks.
```ts
new Job(client, {
client.defineJob({
id: "new-user",
name: "Run when a new user signs up",
version: "0.0.1",
@@ -44,13 +44,9 @@ new Job(client, {
await io.wait("wait", 60 * 60 * 3); // wait for 3 hours
// You can wrap your own code in a Task, for retrying, resumability and logging
const response = await io.runTask(
"my-task",
{ name: "My Task" },
async () => {
return await longRunningCode(payload.userId);
}
);
const response = await io.runTask("my-task", { name: "My Task" }, async () => {
return await longRunningCode(payload.userId);
});
return response;
},
@@ -76,28 +72,16 @@ The first param of all Tasks is a `key`. This is a unique identifier for the Tas
## References
<CardGroup cols={2}>
<Card
title="Resumability"
icon="clock"
href="/documentation/concepts/resumability"
>
<Card title="Resumability" icon="clock" href="/documentation/concepts/resumability">
Runs can be very long-running. Learn how we handle this.
</Card>
<Card
title="Integrations"
icon="grid-2"
href="/documentation/concepts/integrations"
>
<Card title="Integrations" icon="grid-2" href="/documentation/concepts/integrations">
Integrations utilize Tasks.
</Card>
<Card title="`io` SDK Reference" icon="wrench" href="/sdk/io">
The `io` object allows you to easily run a Task yourself.
</Card>
<Card
title="Viewing Runs Dashboard"
icon="globe"
href="/documentation/guides/viewing-runs"
>
<Card title="Viewing Runs Dashboard" icon="globe" href="/documentation/guides/viewing-runs">
View all Runs for a Job, all the way down to individual Tasks.
</Card>
</CardGroup>
@@ -17,7 +17,7 @@ const dynamicSchedule = new DynamicSchedule(client, {
});
//2. create a Job that is attached to the dynamic schedule
new Job(client, {
client.defineJob({
id: "user-dynamicinterval",
name: "User Dynamic Interval",
version: "0.1.1",
@@ -41,7 +41,7 @@ async function registerUserCronJob(userId: string, userSchedule: string) {
}
//5. Register inside other Jobs
new Job(client, {
client.defineJob({
id: "register-dynamicinterval",
name: "Register Dynamic Interval",
version: "0.1.1",
@@ -77,7 +77,7 @@ const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, {
});
//2. create a Job that is attached to the dynamic trigger
new Job(client, {
client.defineJob({
id: "listen-for-dynamic-trigger",
name: "Listen for dynamic trigger",
version: "0.1.1",
@@ -87,9 +87,7 @@ new Job(client, {
},
run: async (payload, io, ctx) => {
await io.slack.postMessage("Slack 📝", {
text: `New Issue opened on repo: ${
payload.issue.html_url
}. \n\n${JSON.stringify(ctx)}`,
text: `New Issue opened on repo: ${payload.issue.html_url}. \n\n${JSON.stringify(ctx)}`,
channel: "C04GWUTDC3W",
});
},
@@ -105,7 +103,7 @@ async function registerRepo(owner: string, repo: string) {
}
//4. Register inside other Jobs
new Job(client, {
client.defineJob({
id: "new-repo",
name: "New repo",
version: "0.1.1",
@@ -23,7 +23,7 @@ You can always start out by using `z.any()` as your schema, and then later on yo
## Example
```ts
new Job(client, {
client.defineJob({
id: "new-user-slack",
name: "New user slack message",
version: "0.1.0",
@@ -58,9 +58,8 @@ new Job(client, {
```
<Note>
You can subscribe to the same event from multiple different Jobs. This is
useful if you want to send an event to multiple different services or if you
want to keep each Job small and simple.
You can subscribe to the same event from multiple different Jobs. This is useful if you want to
send an event to multiple different services or if you want to keep each Job small and simple.
</Note>
## Sending events
@@ -84,7 +83,7 @@ await client.sendEvent({
You can use `io.sendEvent()` to send events from inside a Job run, to trigger another. [View the SDK reference](/sdk/io/sendevent).
```ts
new Job(client, {
client.defineJob({
id: "event-1",
name: "Run when the foo.bar event happens",
version: "0.0.1",
@@ -15,7 +15,7 @@ This job will run every 60 seconds, starting 60 seconds after this Job is first
```ts
import { Job, intervalTrigger } from "@trigger.dev/sdk";
new Job(client, {
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
@@ -43,7 +43,7 @@ This job will run at 2:30pm every Monday. You can get help with [CRON syntax](ht
```ts
import { Job, cronTrigger } from "@trigger.dev/sdk";
new Job(client, {
client.defineJob({
id: "scheduled-job-2",
name: "Scheduled Job 2",
version: "0.1.1",
@@ -32,7 +32,7 @@ const github = new Github({
token: process.env.GITHUB_API_KEY!,
});
new Job(client, {
client.defineJob({
id: "critical-issue-alert",
name: "Critical Issue Alert",
version: "0.1.0",
@@ -42,7 +42,7 @@ const slack = new Slack({
id: "slack",
});
new Job(client, {
client.defineJob({
id: "critical-issue-alert",
name: "Critical Issue Alert",
version: "0.1.0",
+262
View File
@@ -0,0 +1,262 @@
---
title: Using Manual Setup
description: How to Manually Initialize Trigger.dev in your Next.js project
---
<Accordion defaultOpen title="Don't have a Next.js project yet to add Trigger.dev to? No problem, you can complete the Manual Setup using a blank Next.js project:">
Create a blank project by running the `create-next-app` command in your terminal:
```bash
npx create-next-app@latest
```
Trigger.dev works with either the Pages or App Router configuration.
</Accordion>
## Installing Required Packages
To begin, install the necessary packages in your Next.js project directory. You can choose one of the following package managers:
<CodeGroup>
```bash npm
npm i @trigger.dev/sdk @trigger-dev/nextjs
```
```bash pnpm
pnpm install @trigger.dev/sdk @trigger-dev/nextjs
```
```bash yarn
yarn add @trigger.dev/sdk @trigger-dev/nextjs
```
</CodeGroup>
<br />
<Note>Ensure that you execute this command within a Next.js project.</Note>
## 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.local` 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` 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:**
Open the configuration file you created and add the following code:
```typescript
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "my-app",
apiKey: process.env.TRIGGER_API_KEY,
apiUrl: process.env.TRIGGER_API_URL,
});
```
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
4. **File Location:**
Depending on your project structure, save the configuration file in the appropriate location:
- If your project uses a **src** directory, save the file within the **src** directory.
- If your project does not use a **src** directory, save the file in the project root.
**Example Directory Structure with src:**
```
project-root/
├── src/
├── trigger.ts
├── other files...
```
**Example Directory Structure without src:**
```
project-root/
├── trigger.ts
├── other files...
```
By following these steps, you'll configure the Trigger Client to work with your project, regardless of whether you have a separate **src** directory and whether you're using TypeScript or JavaScript files.
## Creating the API Route
To establish an API route for interacting with Trigger.dev, follow these steps based on your project's file type and structure
<Tabs>
<Tab title="App Directory">
1. Create a new file named `route.(ts/js)` within the `app/api/trigger/` directory.
2. Add the following code to `route.(ts/js)`:
```typescript
import { createAppRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger";
import "@/Jobs";
export const { POST, dynamic } = createAppRoute(client);
```
</Tab>
<Tab title="Pages Directory">
1. Create a new file named `trigger.(ts/js)` within the `pages/api/` directory.
2. Add the following code to `trigger.(ts/js)`:
```
import { createPagesRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger";
import "@/Jobs";
const { handler, config } = createPagesRoute(client);
export { config };
export default handler;
```
</Tab>
</Tabs>
<Warning>In the code blocks, replace "@/trigger" with the appropriate path to your Trigger Client configuration file, and adjust the path to the Jobs folder accordingly. Make sure to provide the correct paths if your project isn't utilizing the Next.js alias feature.</Warning>
## Creating the Example Job
1. Create a folder named `Jobs` alongside your `app` or `pages` directory
2. Inside the `Jobs` folder, add two files named `example.(ts/js)` and `index.(ts/js)`.
<CodeGroup>
```typescript 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!",
};
},
});
```
```typescript index.ts/index.(ts/js)
// import all your job files here
export * from "./examples"
```
</CodeGroup>
## Additonal Job Definations
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/js)`, you can export other job files like this:
```typescript
// import all your job files here
export * from "./examples"
export * from "./other-job-file";
```
## 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 Next.js project locally 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.
![Your first Job](/images/cli-dev.gif)
<Warning>
Make sure your Next.js site is running locally before continuing. You must
also leave this `dev` terminal command running while you develop.
</Warning>
In a **new terminal window or tab** run:
<CodeGroup>
```bash npm
npx @trigger.dev/cli@latest dev
```
```bash pnpm
pnpm dlx @trigger.dev/cli@latest dev
```
```bash yarn
yarn dlx @trigger.dev/cli@latest dev
```
</CodeGroup>
<br />
<Note>
You can optionally pass the port if you're not running on 3000 by adding
`--port 3001` to the end
</Note>
<Tip>If your existing Next.js 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/nextjs#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.</Tip>
@@ -57,7 +57,7 @@ First, cd into your Next.js project, then run the `@trigger.dev/cli init` comman
npx @trigger.dev/cli@latest init -t "https://<your render app name>.onrender.com"
```
When it asks for your development API key, head over to your self-hosted Trigger.dev dashboard and select the initial project you created when signing up, and head to the `Environments & API Keys` page to copy your `dev` API key:
When it asks for your development API key, head over to your self-hosted Trigger.dev dashboard and select the initial project you created when signing up, and head to the `Environments & API Keys` page to copy your `dev` **SERVER** API key:
![api key](/images/api-key.png)
@@ -46,7 +46,7 @@ There are two way to use Integrations in a Job:
This example automatically assigns "matt-aitken" to any new issue in the `trigger.dev` repo (lucky him).
```ts
new Job(client, {
client.defineJob({
id: "assign-on-issue-opened",
name: "Assign on Issue Opened",
version: "0.1.0",
@@ -35,7 +35,7 @@ There are two way to use Integrations in a Job:
This example send a Slack message when someone stars the `trigger.dev` GitHub repo 🤩.
```ts
new Job(client, {
client.defineJob({
id: "star-slack-notification",
name: "New Star Slack Notification",
version: "0.1.0",
+3 -3
View File
@@ -13,7 +13,7 @@ We use it [extensively](https://github.com/search?q=repo%3Atriggerdotdev%2Ftrigg
But there are a few places where we ask you to provide us with a Zod schema, for example when defining your own [events](/documentation/concepts/triggers/events):
```ts
new Job(client, {
client.defineJob({
id: "new-user",
name: "New user",
version: "0.1.0",
@@ -36,8 +36,8 @@ new Job(client, {
So it will help to know a little about Zod and how to use it. We definitely recommend the well written [Zod README](https://github.com/colinhacks/zod#readme) but we've included a short primer below.
<Tip>
Wherever we require you to pass in a Zod schema, you can always start with
`z.any()` which accepts `any` type and then add more strict validations later.
Wherever we require you to pass in a Zod schema, you can always start with `z.any()` which accepts
`any` type and then add more strict validations later.
</Tip>
## Basic Usage
+5 -10
View File
@@ -35,7 +35,7 @@ Once you've created an account, follow the steps to:
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` API key.
2. Copy the `DEV` **SERVER** API key.
![API Keys](/images/api-keys.png)
## Run the CLI `init` command
@@ -62,7 +62,7 @@ yarn dlx @trigger.dev/cli@latest init
It will ask you a few questions
1. Are you using the [Trigger.dev Cloud](https://trigger.dev) or [self-hosting](/documentation/guides/self-hosting)? You're probably using the cloud.
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)
@@ -167,7 +167,7 @@ In there is this Job:
```typescript
//Job definition uses the client
new Job(client, {
client.defineJob({
// 1. Metadata
id: "example-job",
name: "Example Job",
@@ -212,11 +212,7 @@ Congratulations, you should get redirected so you can see your first Run!
## What's next?
<CardGroup cols={2}>
<Card
title="Write your first Job"
icon="hexagon-plus"
href="/documentation/guides/create-a-job"
>
<Card title="Write your first Job" icon="hexagon-plus" href="/documentation/guides/create-a-job">
A Guide for how to create your first real Job
</Card>
<Card
@@ -227,8 +223,7 @@ Congratulations, you should get redirected so you can see your first Run!
Learn more about how Trigger.dev works and how it can help you.
</Card>
<Card title="Examples" icon="slot-machine" href="/examples">
One of the quickest ways to learn how Trigger.dev works is to view some
example Jobs.
One of the quickest ways to learn how Trigger.dev works is to view some example Jobs.
</Card>
<Card title="Get help" icon="hire-a-helper" href="/documentation/get-help">
Struggling getting setup or have a question? We're here to help.
+7 -4
View File
@@ -4,21 +4,24 @@ description: "An ever-growing list of example Jobs which you can use to get star
---
<Info>
If you are using integrations, you'll need set up authentication either using
OAuth or API keys / access tokens. You can find out how to do that in the
[integrations section](/integrations).
If you are using integrations, you'll need set up authentication either using OAuth or API keys /
access tokens. You can find out how to do that in the [integrations section](/integrations).
</Info>
Click the links below to view the Job code. You can also easily test these Jobs by following the instructions in the README of each example.
| Job (code in link) | Description | Integrations used |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [Basic delay](https://github.com/triggerdotdev/examples/blob/main/delays/src/jobs/delayJob.ts) | Logs a message to the console, waits for 5 minutes, and then logs another message. | N/A |
| [Basic interval](https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/interval.ts) | This Job will run every 60 seconds, starting 60 seconds after this Job is first indexed. | N/A |
| [Cron scheduled interval](https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/cronScheduled.ts) | A scheduled Job which runs at 2:30pm every Monday. | N/A |
| [OpenAI text summarizer](https://github.com/triggerdotdev/examples/blob/main/openai-text-summarizer/src/jobs/textSummarizer.ts) | Summarizes a block of text, pulling out the most unique and helpful points using OpenAI GPT-3.5 turbo. | [OpenAI](/integrations/apis/openai) |
| [Tell me a joke using OpenAI](https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/tellMeAJoke.ts) | Generates a random joke using OpenAI GPT 3.5. | [OpenAI](/integrations/apis/openai) |
| [Generate an image using OpenAI](https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/generateHedgehogImages.ts) | Generates a random image of a hedgehog using OpenAI DALL-E. | [OpenAI](/integrations/apis/openai) |
| [GitHub issue reminder](https://github.com/triggerdotdev/examples/blob/main/github-issue-reminder/jobs/githubIssue.ts) | Sends a Slack message to a channel if a GitHub issue is left open for 24 hours | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Github new star alert in Slack](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts) | When a repo is starred, a message is sent to a Slack channel with the name and URL of the GitHub user who starred the repo, and the updated Stargazers count. | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Add a custom label to a GitHub issue when it is created](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/onIssueOpened.ts) | When a new GitHub issue is opened it adds a "Bug" label to it. | [GitHub](/integrations/apis/github) |
| [GitHub new star alert](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarAlert.ts) | When a repo is starred a message is logged with the new Stargazers count. | [GitHub](/integrations/apis/github) |
| [Github new star alert in Slack](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts) | When a repo is starred, a message is sent to a Slack channel with the name and URL of the GitHub user who starred the repo, and the updated Stargazers count. | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Send a Slack message when an event is received](https://github.com/triggerdotdev/examples/blob/main/slack/src/jobs/sendSlackMessage.ts) | Sends a Slack message to a specific channel when an event is received. | [Slack](/integrations/apis/slack) |
| [Send an email using Resend](https://github.com/triggerdotdev/examples/blob/main/resend/src/jobs/resendBasicEmail.ts) | Send a basic email using Resend | [Resend](/integrations/apis/resend) |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+1 -1
View File
@@ -23,7 +23,7 @@ title: Tasks
## Usage
```ts
new Job(client, {
client.defineJob({
id: "github-integration-on-issue-opened",
name: "GitHub Integration - On Issue Opened",
version: "0.1.0",
+1 -1
View File
@@ -29,7 +29,7 @@ const github = new Github({
token: process.env.GITHUB_TOKEN!,
});
new Job(client, {
client.defineJob({
id: "github-integration-on-issue",
name: "GitHub Integration - On Issue",
version: "0.1.0",
+11 -16
View File
@@ -45,8 +45,7 @@ const github2 = new Github({
<CardGroup cols={2}>
<Card title="Triggers" icon="stars" href="/integrations/apis/github-triggers">
Trigger Jobs when events happen in GitHub, such as a new commit or a new
issue.
Trigger Jobs when events happen in GitHub, such as a new commit or a new issue.
</Card>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/github-tasks">
Perform tasks such as creating a new issue or a new comment.
@@ -58,8 +57,8 @@ const github2 = new Github({
You can use the underlying client to do anything Octokit supports. In this example we create a project card when a new issue is opened..
<Info>
View [the official GitHub docs](https://docs.github.com/en/rest) for
everything that is supported{" "}
View [the official GitHub docs](https://docs.github.com/en/rest) for everything that is
supported{" "}
</Info>
```ts
@@ -70,7 +69,7 @@ const github = new Github({
token: process.env.GITHUB_TOKEN!,
});
new Job(client, {
client.defineJob({
id: "alert-on-new-github-issues",
name: "Alert on new GitHub issues",
version: "0.1.1",
@@ -84,17 +83,13 @@ new Job(client, {
},
run: async (payload, io, ctx) => {
//wrap the SDK call in runTask
const { data } = await io.runTask(
"create-card",
{ name: "Create card" },
async () => {
//create a project card using the underlying client
return io.github.client.rest.projects.createCard({
column_id: 123,
note: "test",
});
}
);
const { data } = await io.runTask("create-card", { name: "Create card" }, async () => {
//create a project card using the underlying client
return io.github.client.rest.projects.createCard({
column_id: 123,
note: "test",
});
});
//log the url of the created card
await io.logger.info(data.url);
+241 -20
View File
@@ -2,10 +2,17 @@
title: Introduction
---
Trigger.dev provides seamless integration with OpenAI, enabling developers to harness the power of AI
language models in their serverless applications. With Trigger.dev's background tasks, long-running
OpenAI completions become possible, even within the constraints of serverless timeouts.
<Snippet file="integration-getting-started.mdx" />
## Installation
To get started with the OpenAI integration on Trigger.dev, you need to install the `@trigger.dev/openai` package.
You can do this using npm, pnpm, or yarn:
<CodeGroup>
```bash npm
@@ -13,7 +20,7 @@ npm install @trigger.dev/openai@latest
```
```bash pnpm
pnpm install @trigger.dev/openai@latest
pnpm add @trigger.dev/openai@latest
```
```bash yarn
@@ -24,7 +31,8 @@ yarn add @trigger.dev/openai@latest
## Authentication
OpenAI supports API Keys
To use the OpenAI API with Trigger.dev, you'll need an API Key from OpenAI.
If you don't have one yet, you can obtain it from the [OpenAI dashboard](https://platform.openai.com/account/api-keys).
```ts
import { OpenAI } from "@trigger.dev/openai";
@@ -35,10 +43,12 @@ const openai = new OpenAI({
});
```
## Example
## Usage
Include the OpenAI integration in your Trigger.dev job:
```ts
new Job(client, {
client.defineJob({
id: "openai-tasks",
name: "OpenAI Tasks",
version: "0.0.1",
@@ -50,20 +60,11 @@ new Job(client, {
openai,
},
run: async (payload, io, ctx) => {
const response = await io.openai.backgroundCreateChatCompletion(
"background-chat-completion",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}
);
await io.logger.info("choices", response.choices);
// Now you can access the OpenAI tasks through the io object
await io.openai.createCompletion("completion", {
model: "davinci",
prompt: "Once upon a time",
});
},
});
```
@@ -75,13 +76,15 @@ Tasks that are marked as "long-running" can last longer than your serverless tim
| Function Name | Description | Long-running? |
| -------------------------------- | ------------------------------------------------------------------------- | ------------- |
| `createCompletion` | Generates text completions given a prompt. |
| `backgroundCreateCompletion` | Generates text completions in the background. | ✔ |
| `backgroundCreateCompletion` | Generates text completions in the background. | ✔ |
| `createChatCompletion` | Generates text completions in a conversational context. |
| `backgroundCreateChatCompletion` | Generates text completions in a conversational context in the background. | ✔ |
| `backgroundCreateChatCompletion` | Generates text completions in a conversational context in the background. | ✔ |
| `retrieveModel` | Retrieves a specific model by ID. |
| `listModels` | Lists the available models. |
| `createEdit` | Edits a given text prompt. |
| `createImage` | Generates images from textual descriptions. |
| `createImageEdit` | Creates an edited or extended image given an original image and a prompt |
| `createImageVariation` | Creates a variation of a given image. |
| `createEmbedding` | Generates embeddings for a given text. |
| `createFile` | Uploads a file to the OpenAI API. |
| `listFiles` | Lists the uploaded files. |
@@ -92,3 +95,221 @@ Tasks that are marked as "long-running" can last longer than your serverless tim
| `cancelFineTune` | Cancels a specific fine-tune by ID. |
| `listFineTuneEvents` | Lists the events for a specific fine-tune by ID. |
| `deleteFineTune` | Deletes a specific fine-tune by ID. |
## Examples
### Generate a joke
Here's an example of how to use the OpenAI integration in a Trigger.dev job.
In this example, we'll create a background task to generate a programming joke.
```ts
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 response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
});
```
### Generate Code Snippets
In this example, we'll leverage Trigger.dev's background task to generate code snippets for
a given programming task:
```ts
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 programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
});
```
### Summarize Text
We'll use Trigger.dev's background task to summarize a lengthy article:
```ts
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 articleToSummarize = `Lorem ipsum. olor sit amet, consectetur adipiscing elit.
Sed nec aliquet sapien. Pellentesque vitae nisi id purus luctus tincidunt.
Proin condimentum malesuada turpis, eget tincidunt mauris viverra in.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Please summarize the following article:\n\n${articleToSummarize}`,
});
await io.logger.info("summary", response.choices[0]?.text);
},
});
```
### Draft Email Response
we'll use Trigger.dev's background task to draft an email response based on a given email content:
```ts
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 emailContent = `Dear John,
Thank you for your inquiry. We appreciate your interest in our products.
I have reviewed your request, and I'm pleased to inform you that we can
accommodate your requirements. Please find the attached proposal for your
reference. If you have any further questions, feel free to ask.
Best regards,
Jane Doe`;
const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: emailContent,
},
{
role: "assistant",
content: "Draft a suitable response to the email above.",
},
],
});
await io.logger.info("draftedEmailResponse", response.choices[0]?.text);
},
});
```
### Chatbot Counseling Session
This job represents a simulated AI counseling session. Leveraging OpenAI's ability to understand context and generate human-like text, it forms empathetic responses to user inputs. Such a system could be part of a mental wellness app.
```ts
client.defineJob({
id: "openai-chatbot-counseling",
name: "Chatbot Counseling Session",
version: "0.0.1",
trigger: eventTrigger({
name: "openai.startCounselingSession",
schema: z.object({}),
}),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
const response = await io.openai.backgroundCreateChatCompletion(
"background-counseling-chat-completion",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "You are a helpful and empathetic AI counselor.",
},
{
role: "user",
content: "I've been feeling really stressed out lately.",
},
],
}
);
await io.logger.info("counseling session", response.choices);
},
});
```
### AI Roleplay Game Session
This job creates a fantasy AI role-playing game. It could be fun for interactive storytelling or game development contexts.
```ts
client.defineJob({
id: "openai-roleplay-game-session",
name: "AI Roleplay Game Session",
version: "0.0.1",
trigger: eventTrigger({
name: "openai.startRoleplayGameSession",
schema: z.object({}),
}),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
const response = await io.openai.backgroundCreateChatCompletion(
"background-roleplay-game-session-chat-completion",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "You are an intelligent guide in a fantasy role-playing game.",
},
{
role: "user",
content: "I embark on a quest for the enchanted crown. What's the first step?",
},
],
}
);
await io.logger.info("roleplay game session", response.choices);
},
});
```
+26 -29
View File
@@ -50,7 +50,7 @@ export const plain = new Plain({
apiKey: process.env.PLAIN_API_KEY!,
});
new Job(client, {
client.defineJob({
id: "plain-playground",
name: "Plain Playground",
version: "0.1.1",
@@ -87,37 +87,34 @@ new Job(client, {
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`,
},
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,
},
},
{
componentDivider: {
dividerSpacingSize: ComponentDividerSpacingSize.M,
},
{
componentText: {
textSize: ComponentTextSize.S,
textColor: ComponentTextColor.Muted,
text: "External id",
},
},
{
componentText: {
textSize: ComponentTextSize.S,
textColor: ComponentTextColor.Muted,
text: "External id",
},
{
componentText: {
text: foundCustomer?.externalId ?? "",
},
},
{
componentText: {
text: foundCustomer?.externalId ?? "",
},
],
}
);
},
],
});
},
});
```
@@ -145,7 +142,7 @@ export const plain = new Plain({
apiKey: process.env.PLAIN_API_KEY!,
});
new Job(client, {
client.defineJob({
id: "plain-client",
name: "Plain Client",
version: "0.1.0",
+1 -1
View File
@@ -51,7 +51,7 @@ const resend = new Resend({
apiKey: process.env.RESEND_API_KEY!,
});
new Job(client, {
client.defineJob({
id: "send-resend-email",
name: "Send Resend Email",
version: "0.1.0",
+1 -1
View File
@@ -37,7 +37,7 @@ const slack = new Slack({
## Example
```ts
new Job(client, {
client.defineJob({
id: "slack-test",
name: "Slack test",
version: "0.0.1",
+15 -23
View File
@@ -7,8 +7,8 @@ description: "Interact with your Supabase project using the Supabase JS Client."
Our `@trigger.dev/supabase` package provides an integration that wraps the [@supabase/supabase-js](https://github.com/supabase/supabase-js) package, allowing you to run tasks to interact with your Supabase project.
<Note>
If you want to trigger jobs based on changes in your Supabase database, you'll
need to use the [Supabase Management API](../management) integration
If you want to trigger jobs based on changes in your Supabase database, you'll need to use the
[Supabase Management API](/integrations/apis/supabase/management) integration
</Note>
## Usage
@@ -26,8 +26,7 @@ const supabase = new Supabase({
```
<Warning>
Never expose the `service_role` key in a browser or anywhere where a user can
see it.
Never expose the `service_role` key in a browser or anywhere where a user can see it.
</Warning>
You can then use the `supabase` integration to run tasks in your jobs:
@@ -39,21 +38,17 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const { data: users, error } = await io.supabase.runTask(
"find-users",
async (db) => {
return db.from("users").select("*");
}
);
const { data: todos, error } = await io.supabase.runTask("find-todos", async (db) => {
return db.from("todos").select("*");
});
},
});
```
<Note>
By using `runTask` instead of the `@supabase/supabase-js` client directly
inside your job run, you'll be able to create tasks that can be run
idempotently and also retried. For more, see our guide on
[Resumability](http://localhost:3050/documentation/concepts/resumability)
By using `runTask` instead of the `@supabase/supabase-js` client directly inside your job run,
you'll be able to create tasks that can be run idempotently and also retried. For more, see our
guide on [Resumability](http://localhost:3050/documentation/concepts/resumability)
</Note>
You can also choose to throw an error if the query fails and abort the job run:
@@ -65,8 +60,8 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const users = await io.supabase.runTask("find-users", async (db) => {
const { data, error } = await db.from("users").select("*");
const todos = await io.supabase.runTask("find-todos", async (db) => {
const { data, error } = await db.from("todos").select("*");
if (error) throw error;
@@ -83,10 +78,7 @@ The `db` object passed to the callback is an instance of the [@supabase/supabase
- [Invoking Functions](https://supabase.com/docs/reference/javascript/functions-invoke)
- [Storage](https://supabase.com/docs/reference/javascript/storage-createbucket)
<Warning>
Currently we do not support Supabase Realtime (such as subscribing to a
channel)
</Warning>
<Warning>Currently we do not support Supabase Realtime (such as subscribing to a channel)</Warning>
## Typescript Support
@@ -108,15 +100,15 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const users = await io.supabase.runTask("find-users", async (db) => {
const { data, error } = await db.from("users").select("*");
const todos = await io.supabase.runTask("find-todos", async (db) => {
const { data, error } = await db.from("todos").select("*");
if (error) throw error;
return data;
});
// users is now typed as User[] instead of any[]
// todos is now typed as Todo[] instead of any[]
},
});
```
+52 -20
View File
@@ -85,6 +85,28 @@ For a full list of available tasks, see the [Supabase Management API](https://su
The `SupabaseManagement` integration also provides the ability to trigger jobs based on changes in your Supabase database through the use of [Supabase Database Webhooks](https://supabase.com/docs/guides/database/webhooks).
### Enable Database Webhooks
<Info>
Manually enabling database webhooks are only needed if you are using `@trigger.dev/supabase` at
version `2.0.2` or earlier. If you are using `2.0.3` or later, this is done automatically for you.
</Info>
Currently the Supabase Management API does not provide a way to enable database webhooks, so you'll need to do this manually.
You can do this by visiting your [Database Webhooks settings](https://supabase.com/dashboard/project/_/database/hooks) and clicking the "Enable webhooks" button:
![Enable Database Webhooks](/images/supabase-enable-webhooks.png)
You'll have to do this for each Supabase project you want to use webhooks with.
<Note>
You don't actually need to create any webhooks yourself, our integration will take care of that
part for you.
</Note>
### Usage
To use this feature, you'll first initialize a `db` instance, passing in your Supabase project [ID](https://supabase.com/dashboard/project/_/settings/api) (or URL):
```ts
@@ -104,7 +126,7 @@ client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onInserted({
table: "users",
table: "todos",
}),
run: async (payload, io, ctx) => {
// payload is the database webhook body (see https://supabase.com/docs/guides/database/webhooks#payload)
@@ -115,20 +137,6 @@ client.defineJob({
You can add additional filters to the trigger by passing a `filter` object:
```ts
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onUpdated({
table: "users",
filter: {
country: ["USA", "Canada"], // This will only trigger the job if the user.country is USA or Canada
},
}),
run: async (payload, io, ctx) => {
// payload is the database webhook body (see https://supabase.com/docs/guides/database/webhooks#payload)
},
});
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
@@ -150,10 +158,34 @@ client.defineJob({
});
```
You can also listen for multiple different events using the `on` trigger:
```ts
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.on({
table: "todos",
events: ["INSERT", "UPDATE"] // Trigger on both insert and update events
filter: {
record: {
is_completed: [false],
},
},
}),
run: async (payload, io, ctx) => {
if (payload.type === "INSERT") {
// payload will be typed as the INSERT payload
} else {
// payload will be typed as the UPDATE payload
}
},
});
```
<Note>
We will only create at most 1 database webhook per table, to limit resource
usage when writing to your database. This means we cannot support scoping
updated triggers to specific columns.
We will only create at most 1 database webhook per table, to limit resource usage when writing to
your database. This means we cannot support scoping updated triggers to specific columns.
</Note>
### Typescript Support
@@ -175,10 +207,10 @@ client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onUpdated({
table: "users",
table: "todos",
}),
run: async (payload, io, ctx) => {
// payload.record and payload.old_record are now correctly typed to match the users table
// payload.record and payload.old_record are now correctly typed to match the todos table
},
});
```
+6 -9
View File
@@ -50,7 +50,7 @@ export const typeform = new Typeform({
token: process.env.TYPEFORM_API_KEY!,
});
new Job(client, {
client.defineJob({
id: "do-something-on-new-responses",
name: "Send a message to slack on new responses",
version: "0.1.1",
@@ -90,7 +90,7 @@ const typeform = new Typeform({
token: process.env.TYPEFORM_PAT!,
});
new Job(client, {
client.defineJob({
id: "typeform-tasks",
name: "Typeform Tasks",
version: "0.1.0",
@@ -110,12 +110,9 @@ new Job(client, {
pageSize: 50,
});
const allResponses = await io.typeform.getAllResponses(
"get-all-responses",
{
uid: payload.formId,
}
);
const allResponses = await io.typeform.getAllResponses("get-all-responses", {
uid: payload.formId,
});
},
});
```
@@ -133,7 +130,7 @@ const typeform = new Typeform({
token: process.env.TYPEFORM_PAT!,
});
new Job(client, {
client.defineJob({
id: "typeform-client",
name: "Typeform Client",
version: "0.1.0",
+22 -31
View File
@@ -71,9 +71,9 @@ Once you've created your Integration package, you can start developing it. In th
This is the entry point of the Integration package. It exports a main "integration" class that implements the `TriggerIntegration` interface. For example, the `@trigger.dev/github` Integration exports a `Github` class that implements.
<Tip>
We're adopting the naming convention of naming the class after the service,
without a suffix or prefix. We prefer the exported name be `Slack` instead of
something like `SlackIntegration` or `SlackConnector`
We're adopting the naming convention of naming the class after the service, without a suffix or
prefix. We prefer the exported name be `Slack` instead of something like `SlackIntegration` or
`SlackConnector`
</Tip>
<Accordion title="Example: OpenAI">
@@ -84,9 +84,7 @@ import { Configuration, OpenAIApi } from "openai";
import * as tasks from "./tasks";
import { OpenAIIntegrationOptions } from "./types";
export class OpenAI
implements TriggerIntegration<IntegrationClient<OpenAIApi, typeof tasks>>
{
export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, typeof tasks>> {
client: IntegrationClient<OpenAIApi, typeof tasks>;
constructor(private options: OpenAIIntegrationOptions) {
@@ -121,19 +119,18 @@ export class OpenAI
The `TriggerIntegration` interface requires three properties to be implemented:
<ParamField body="id" type="string" required>
The `id` that uniquely identifies the Integration. This should always be
passed through the constructor options.
The `id` that uniquely identifies the Integration. This should always be passed through the
constructor options.
</ParamField>
<ParamField body="metadata" type="object" required>
<Expandable title="properties">
<ParamField body="id" type="string" required>
A unique identifier for the Integration. For example, the OpenAI
Integration has an id of `"openai"`.
A unique identifier for the Integration. For example, the OpenAI Integration has an id of
`"openai"`.
</ParamField>
<ParamField body="name" type="string" required>
The name of the Integration. For example, the OpenAI Integration has a
name of `"OpenAI"`.
The name of the Integration. For example, the OpenAI Integration has a name of `"OpenAI"`.
</ParamField>
</Expandable>
</ParamField>
@@ -194,11 +191,7 @@ For example, here is the `getForm` authenticated task defined in the `@trigger.d
import type { AuthenticatedTask } from "@trigger.dev/sdk";
import type { GetFormParams, GetFormResponse, TypeformSDK } from "./types";
export const getForm: AuthenticatedTask<
TypeformSDK,
GetFormParams,
GetFormResponse
> = {
export const getForm: AuthenticatedTask<TypeformSDK, GetFormParams, GetFormResponse> = {
init: (params) => {
return {
name: "Get Form",
@@ -232,7 +225,7 @@ export type GetFormResponse = Prettify<Typeform.Form>;
```
```ts usage.ts
new Job(client, {
client.defineJob({
id: "typeform-playground",
name: "Typeform Playground",
version: "0.1.1",
@@ -258,9 +251,9 @@ The first thing to notice is the explicit typing of the `getForm` export as an `
If you take a look at the `usage.ts` file above, you can see how this task is used in a job. The `io.typeform.getForm` function is typed as returning `Promise<GetFormResponse>` and the `params` argument is typed as `GetFormParams`.
<Note>
Notice how the params are the _second_ argument to `getForm`, that's because
the first argument is always the task key. See our [Keys and Resumability
docs](/documentation/concepts/resumability) for more on why this is important
Notice how the params are the _second_ argument to `getForm`, that's because the first argument is
always the task key. See our [Keys and Resumability docs](/documentation/concepts/resumability)
for more on why this is important
</Note>
#### `run` function
@@ -268,8 +261,8 @@ If you take a look at the `usage.ts` file above, you can see how this task is us
The `run` function is the main function that will be called when the task is run. It's an async function that takes up to 5 arguments:
<ParamField body="params" type="type parameter" required>
The input params that were passed to the task. This is the second argument to
the `getForm` function in the example above.
The input params that were passed to the task. This is the second argument to the `getForm`
function in the example above.
</ParamField>
<ParamField body="client" type="type parameter" required>
@@ -286,9 +279,9 @@ The `run` function is the main function that will be called when the task is run
</ParamField>
<ParamField body="auth" type="ConnectionAuth">
If for some reason you need to access the auth object that was used to seed
the SDK client, you can access it here. The `AuthenticatedTask` generic type
takes an optional 4th type parameter that allows you to specify the auth type
If for some reason you need to access the auth object that was used to seed the SDK client, you
can access it here. The `AuthenticatedTask` generic type takes an optional 4th type parameter that
allows you to specify the auth type
</ParamField>
#### `init` function
@@ -296,8 +289,8 @@ The `run` function is the main function that will be called when the task is run
The `init` function is used to initialize the task. It's a synchronous function that takes a single argument:
<ParamField body="params" type="type parameter" required>
The input params that were passed to the task. This is the second argument to
the `getForm` function in the example above.
The input params that were passed to the task. This is the second argument to the `getForm`
function in the example above.
</ParamField>
#### `onError` function
@@ -466,9 +459,7 @@ export const backgroundCreateCompletion: AuthenticatedTask<
headers: {
"Content-Type": "application/json",
Authorization: redactString`Bearer ${auth.apiKey}`,
...(auth.organization
? { "OpenAI-Organization": auth.organization }
: {}),
...(auth.organization ? { "OpenAI-Organization": auth.organization } : {}),
},
body: JSON.stringify(params),
}
+11 -27
View File
@@ -23,7 +23,8 @@
},
"feedback": {
"suggestEdit": true,
"raiseIssue": true
"raiseIssue": true,
"thumbsRating": true
},
"topbarCtaButton": {
"type": "github",
@@ -102,6 +103,7 @@
]
},
"documentation/guides/cli",
"documentation/guides/manual",
"documentation/guides/running-jobs",
{
"group": "Using the Dashboard",
@@ -154,10 +156,7 @@
},
{
"group": "Overview",
"pages": [
"integrations/introduction",
"integrations/create"
]
"pages": ["integrations/introduction", "integrations/create"]
},
{
"group": "Integrations",
@@ -180,22 +179,16 @@
},
{
"group": "OpenAI",
"pages": [
"integrations/apis/openai"
]
"pages": ["integrations/apis/openai"]
},
"integrations/apis/plain",
{
"group": "Resend",
"pages": [
"integrations/apis/resend"
]
"pages": ["integrations/apis/resend"]
},
{
"group": "Slack",
"pages": [
"integrations/apis/slack"
]
"pages": ["integrations/apis/slack"]
},
"integrations/apis/typeform"
]
@@ -249,10 +242,7 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
}
]
},
@@ -263,10 +253,7 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
}
]
},
@@ -287,10 +274,7 @@
},
{
"group": "Overview",
"pages": [
"examples/introduction",
"examples/examples-repository"
]
"pages": ["examples/introduction", "examples/examples-repository"]
}
],
"footerSocials": {
@@ -303,4 +287,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+3 -4
View File
@@ -23,8 +23,7 @@ A useful tool when writing CRON expressions is [crontab guru](https://crontab.gu
<ResponseField name="options" type="object" required>
<Expandable title="options" defaultOpen>
<ResponseField name="cron" type="string" required>
A CRON expression that defines the schedule. Note that the timezone used
is always UTC.
A CRON expression that defines the schedule. Note that the timezone used is always UTC.
</ResponseField>
</Expandable>
</ResponseField>
@@ -32,7 +31,7 @@ A useful tool when writing CRON expressions is [crontab guru](https://crontab.gu
<RequestExample>
```typescript 9am UTC everyday
new Job(client, {
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
@@ -51,7 +50,7 @@ new Job(client, {
```
```typescript First day of month
new Job(client, {
client.defineJob({
id: "scheduled-job-2",
name: "Scheduled Job 2",
version: "0.1.1",
+2 -2
View File
@@ -39,7 +39,7 @@ const dynamicSchedule = new DynamicSchedule(client, {
});
//2. create a Job that is attached to the dynamic schedule
new Job(client, {
client.defineJob({
id: "user-dynamicinterval",
name: "User Dynamic Interval",
version: "0.1.1",
@@ -63,7 +63,7 @@ async function registerUserCronJob(userId: string, userSchedule: string) {
}
//5. Register inside other Jobs
new Job(client, {
client.defineJob({
id: "register-dynamicinterval",
name: "Register Dynamic Interval",
version: "0.1.1",
+3 -5
View File
@@ -40,7 +40,7 @@ const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, {
});
//2. create a Job that is attached to the dynamic trigger
new Job(client, {
client.defineJob({
id: "listen-for-dynamic-trigger",
name: "Listen for dynamic trigger",
version: "0.1.1",
@@ -50,9 +50,7 @@ new Job(client, {
},
run: async (payload, io, ctx) => {
await io.slack.postMessage("Slack 📝", {
text: `New Issue opened on repo: ${
payload.issue.html_url
}. \n\n${JSON.stringify(ctx)}`,
text: `New Issue opened on repo: ${payload.issue.html_url}. \n\n${JSON.stringify(ctx)}`,
channel: "C04GWUTDC3W",
});
},
@@ -68,7 +66,7 @@ async function registerRepo(owner: string, repo: string) {
}
//4. Register inside other Jobs
new Job(client, {
client.defineJob({
id: "new-repo",
name: "New repo",
version: "0.1.1",
+1 -1
View File
@@ -53,7 +53,7 @@ You can have multiple Jobs that subscribe to the same event, they will all trigg
```typescript eventTrigger()
//this Job subscribes to an event called new.user
new Job(client, {
client.defineJob({
id: "job-2",
name: "Second job",
version: "0.0.1",
+1 -1
View File
@@ -30,7 +30,7 @@ If you wish to Run a Job at an exact time or less frequently than once pr day yo
<RequestExample>
```typescript Every 5 minutes
new Job(client, {
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
+6 -10
View File
@@ -20,9 +20,8 @@ This is used inside the OpenAI Integration for Tasks like `backgroundCreateChatC
The HTTP method to use for the request.
</ResponseField>
<ResponseField name="headers" type="object">
Any headers to send with the request. Note that you can use
[redactString](sdk/redactString) to prevent sensitive information from being
stored (e.g. in the logs), like API keys and tokens.
Any headers to send with the request. Note that you can use [redactString](sdk/redactString) to
prevent sensitive information from being stored (e.g. in the logs), like API keys and tokens.
</ResponseField>
<ResponseField name="body" type="string | ArrayBuffer">
The body of the request.
@@ -84,19 +83,16 @@ An individual retrying strategy can be one of two types:
<Expandable title="headers strategy">
<ResponseField name="type" type="headers" required>
The `headers` strategy retries the request using info from the response
headers.
The `headers` strategy retries the request using info from the response headers.
</ResponseField>
<ResponseField name="limitHeader" type="string">
The header to use to determine the maximum number of times to retry the
request.
The header to use to determine the maximum number of times to retry the request.
</ResponseField>
<ResponseField name="remainingHeader" type="string">
The header to use to determine the number of remaining retries.
</ResponseField>
<ResponseField name="resetHeader" type="string">
The header to use to determine the time when the number of remaining retries
will be reset.
The header to use to determine the time when the number of remaining retries will be reset.
</ResponseField>
</Expandable>
@@ -111,7 +107,7 @@ A `Promise` that resolves after the specified amount of time.
<RequestExample>
```typescript backgroundFetch example
new Job(client, {
client.defineJob({
id: "background-fetch-job",
name: "Background fetch Job",
version: "0.0.1",
+4 -6
View File
@@ -19,9 +19,8 @@ description: "`io.registerCron()` allows you to register a [DynamicSchedule](/sd
<Expandable title="options" defaultOpen>
<ResponseField name="cron" type="string" required>
A CRON expression that defines the schedule. A useful tool when writing CRON
expressions is [crontab guru](https://crontab.guru). Note that the timezone
used is UTC.
A CRON expression that defines the schedule. A useful tool when writing CRON expressions is
[crontab guru](https://crontab.guru). Note that the timezone used is UTC.
</ResponseField>
</Expandable>
@@ -32,8 +31,7 @@ description: "`io.registerCron()` allows you to register a [DynamicSchedule](/sd
A Promise that resolves to an object with the following fields:
<ResponseField name="id" type="string" required>
A unique id for the interval. This is used to identify and unregister the
interval later.
A unique id for the interval. This is used to identify and unregister the interval later.
</ResponseField>
<ResponseField name="metadata" type="any" required>
Any additional metadata about the interval.
@@ -63,7 +61,7 @@ A Promise that resolves to an object with the following fields:
<RequestExample>
```typescript
new Job(client, {
client.defineJob({
id: "my-job",
name: "My job",
version: "0.1.1",
+2 -3
View File
@@ -30,8 +30,7 @@ description: "`io.registerInterval()` allows you to register a [DynamicSchedule]
A Promise that resolves to an object with the following fields:
<ResponseField name="id" type="string" required>
A unique id for the interval. This is used to identify and unregister the
interval later.
A unique id for the interval. This is used to identify and unregister the interval later.
</ResponseField>
<ResponseField name="metadata" type="any" required>
Any additional metadata about the interval.
@@ -61,7 +60,7 @@ A Promise that resolves to an object with the following fields:
<RequestExample>
```typescript
new Job(client, {
client.defineJob({
id: "my-job",
name: "My job",
version: "0.1.1",
+7 -13
View File
@@ -8,16 +8,13 @@ description: "`io.registerTrigger()` allows you to register a [DynamicTrigger](/
<Snippet file="stable-key-param.mdx" />
<ResponseField name="dynamicTrigger" type="DynamicTrigger" required>
A [DynamicTrigger](/sdk/dynamictrigger) that will trigger any Jobs it's
attached.
A [DynamicTrigger](/sdk/dynamictrigger) that will trigger any Jobs it's attached.
</ResponseField>
<ResponseField name="id" type="string" required>
A unique id for the registration. This is used to identify and unregister
later.
A unique id for the registration. This is used to identify and unregister later.
</ResponseField>
<ResponseField name="params" type="object" required>
The params for the DynamicTrigger. These will vary depending on the type of
the DynamicTrigger.
The params for the DynamicTrigger. These will vary depending on the type of the DynamicTrigger.
</ResponseField>
## Returns
@@ -25,8 +22,7 @@ description: "`io.registerTrigger()` allows you to register a [DynamicTrigger](/
A Promise that resolves to an object with the following fields:
<ResponseField name="id" type="string" required>
A unique id for the registration. This is used to identify and unregister
later.
A unique id for the registration. This is used to identify and unregister later.
</ResponseField>
<ResponseField name="key" type="string" required>
The key of the registration.
@@ -43,7 +39,7 @@ const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, {
});
//2. create a Job that is attached to the dynamic trigger
new Job(client, {
client.defineJob({
id: "listen-for-dynamic-trigger",
name: "Listen for dynamic trigger",
version: "0.1.1",
@@ -53,15 +49,13 @@ new Job(client, {
},
run: async (payload, io, ctx) => {
await io.slack.postMessage("Slack 📝", {
text: `New Issue opened on repo: ${
payload.issue.html_url
}. \n\n${JSON.stringify(ctx)}`,
text: `New Issue opened on repo: ${payload.issue.html_url}. \n\n${JSON.stringify(ctx)}`,
channel: "C04GWUTDC3W",
});
},
});
new Job(client, {
client.defineJob({
id: "new-repo",
name: "New repo",
version: "0.1.1",
+2 -2
View File
@@ -122,7 +122,7 @@ A Promise that resolves with the returned value of the callback.
<RequestExample>
```typescript Run a task
new Job(client, {
client.defineJob({
id: "alert-on-new-github-issues",
name: "Alert on new GitHub issues",
version: "0.1.1",
@@ -155,7 +155,7 @@ new Job(client, {
```
```typescript onError callback
new Job(client, {
client.defineJob({
id: "custom-error-handling",
name: "Custom Error handling",
version: "0.1.1",
+3 -4
View File
@@ -13,8 +13,7 @@ Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
<Snippet file="stable-key-param.mdx" />
<ResponseField name="seconds" type="number" required>
The number of seconds to wait. This can be very long, serverless timeouts are
not an issue.
The number of seconds to wait. This can be very long, serverless timeouts are not an issue.
</ResponseField>
<Snippet file="send-event-params.mdx" />
@@ -27,7 +26,7 @@ Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
```typescript Send an event
//this Job sends an event that triggers the second job
new Job(client, {
client.defineJob({
id: "job-1",
name: "First job",
version: "0.0.1",
@@ -45,7 +44,7 @@ new Job(client, {
},
});
new Job(client, {
client.defineJob({
id: "job-2",
name: "Second job",
version: "0.0.1",
+1 -1
View File
@@ -32,7 +32,7 @@ You have two options:
<RequestExample>
```typescript Using io.try()
new Job(client, {
client.defineJob({
id: "get-repo-info",
name: "GitHub get repo info",
version: "0.1.0",
+4 -5
View File
@@ -8,12 +8,11 @@ description: "`io.unregisterCron()` allows you to unregister a [DynamicSchedule]
<Snippet file="stable-key-param.mdx" />
<ResponseField name="dynamicSchedule" type="DynamicSchedule" required>
A [DynamicSchedule](/sdk/dynamicschedule) that will trigger any Jobs it's
attached to on a regular interval.
A [DynamicSchedule](/sdk/dynamicschedule) that will trigger any Jobs it's attached to on a regular
interval.
</ResponseField>
<ResponseField name="id" type="string" required>
A unique id for the schedule. This is used to identify and unregister the
schedule later.
A unique id for the schedule. This is used to identify and unregister the schedule later.
</ResponseField>
## Returns
@@ -27,7 +26,7 @@ A Promise with the following shape:
<RequestExample>
```typescript
new Job(client, {
client.defineJob({
id: "unregister-job",
name: "Unregister dynamic schedule",
version: "0.1.1",
+4 -5
View File
@@ -8,12 +8,11 @@ description: "`io.unregisterInterval()` allows you to unregister a [DynamicSched
<Snippet file="stable-key-param.mdx" />
<ResponseField name="dynamicSchedule" type="DynamicSchedule" required>
A [DynamicSchedule](/sdk/dynamicschedule) that will trigger any Jobs it's
attached to on a regular interval.
A [DynamicSchedule](/sdk/dynamicschedule) that will trigger any Jobs it's attached to on a regular
interval.
</ResponseField>
<ResponseField name="id" type="string" required>
A unique id for the interval. This is used to identify and unregister the
interval later.
A unique id for the interval. This is used to identify and unregister the interval later.
</ResponseField>
## Returns
@@ -27,7 +26,7 @@ A Promise with the following shape:
<RequestExample>
```typescript
new Job(client, {
client.defineJob({
id: "unregister-job",
name: "Unregister dynamic schedule",
version: "0.1.1",
+2 -3
View File
@@ -8,8 +8,7 @@ description: "`io.unregisterTrigger()` allows you to unregister a [DynamicTrigge
<Snippet file="stable-key-param.mdx" />
<ResponseField name="dynamicTrigger" type="DynamicTrigger" required>
A [DynamicTrigger](/sdk/dynamictrigger) that will trigger any Jobs it's
attached to.
A [DynamicTrigger](/sdk/dynamictrigger) that will trigger any Jobs it's attached to.
</ResponseField>
<ResponseField name="id" type="string" required>
A unique id for the trigger. This is used to identify and unregister it later.
@@ -26,7 +25,7 @@ A Promise with the following shape:
<RequestExample>
```typescript
new Job(client, {
client.defineJob({
id: "unregister-job",
name: "Unregister dynamic trigger",
version: "0.1.1",
+1 -1
View File
@@ -32,7 +32,7 @@ You must rethrow the error if this function returns `true`.
<RequestExample>
```typescript Using io.try()
new Job(client, {
client.defineJob({
id: "get-repo-info",
name: "GitHub get repo info",
version: "0.1.0",
+13 -17
View File
@@ -12,7 +12,7 @@ By far the most important thing to understand is the constructor.
<RequestExample>
```ts cronTrigger
new Job(client, {
client.defineJob({
id: "slack-kpi-summary",
name: "Slack kpi summary",
version: "0.1.1",
@@ -35,7 +35,7 @@ new Job(client, {
```
```ts webhook
new Job(client, {
client.defineJob({
id: "github-integration-on-issue",
name: "GitHub Integration - On Issue",
version: "0.1.0",
@@ -52,7 +52,7 @@ new Job(client, {
```
```ts event
new Job(client, {
client.defineJob({
id: "openai-joke",
name: "OpenAI Joke",
version: "0.0.1",
@@ -66,18 +66,15 @@ new Job(client, {
openai,
},
run: async (payload, io, ctx) => {
const joke = await io.openai.backgroundCreateChatCompletion(
"generate-jokes",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: payload.jokePrompt,
},
],
}
);
const joke = await io.openai.backgroundCreateChatCompletion("generate-jokes", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: payload.jokePrompt,
},
],
});
return joke.choices;
},
@@ -89,8 +86,7 @@ new Job(client, {
## Parameters
<ParamField body="client" type="object" required>
An instance of [TriggerClient](/sdk/triggerclient) that is used to send events
to the Trigger API.
An instance of [TriggerClient](/sdk/triggerclient) that is used to send events to the Trigger API.
</ParamField>
<ParamField body="options" type="object" required>
+11 -1
View File
@@ -2,8 +2,18 @@
This project is meant to be used to create a catalog of jobs, usually to test something in an integration or the SDK.
## Setup
You will need to create a `.env` file. You can duplicate the `.env.example` file and set your local `TRIGGER_API_KEY` value.
### Running
You need to build the CLI:
```sh
pnpm run build --filter @trigger.dev/cli
```
Each file in `src` is a separate set of jobs that can be run separately. For example, the `src/stripe.ts` file can be run with:
```sh
@@ -15,7 +25,7 @@ This will open up a local server using `express` on port 8080. Then in a new ter
```sh
cd examples/job-catalog
pnpm run trigger:dev
pnpm run dev:trigger
```
### Adding a new file
+3
View File
@@ -4,6 +4,8 @@
"private": true,
"scripts": {
"stripe": "nodemon --watch src/stripe.ts -r tsconfig-paths/register -r dotenv/config src/stripe.ts",
"supabase": "nodemon --watch src/supabase.ts -r tsconfig-paths/register -r dotenv/config src/supabase.ts",
"supabase:types": "npx supabase gen types typescript --project-id $SUPABASE_PROJECT_ID --schema public --schema auth --schema storage > src/supabase-types.ts",
"dev:trigger": "trigger-cli dev --port 8080"
},
"dependencies": {
@@ -16,6 +18,7 @@
"@trigger.dev/slack": "workspace:*",
"@trigger.dev/stripe": "workspace:*",
"@trigger.dev/typeform": "workspace:*",
"@trigger.dev/supabase": "workspace:*",
"@types/node": "20.4.2",
"typescript": "5.1.6",
"zod": "3.21.4"
+843
View File
@@ -0,0 +1,843 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
auth: {
Tables: {
audit_log_entries: {
Row: {
created_at: string | null
id: string
instance_id: string | null
ip_address: string
payload: Json | null
}
Insert: {
created_at?: string | null
id: string
instance_id?: string | null
ip_address?: string
payload?: Json | null
}
Update: {
created_at?: string | null
id?: string
instance_id?: string | null
ip_address?: string
payload?: Json | null
}
Relationships: []
}
flow_state: {
Row: {
auth_code: string
authentication_method: string
code_challenge: string
code_challenge_method: Database["auth"]["Enums"]["code_challenge_method"]
created_at: string | null
id: string
provider_access_token: string | null
provider_refresh_token: string | null
provider_type: string
updated_at: string | null
user_id: string | null
}
Insert: {
auth_code: string
authentication_method: string
code_challenge: string
code_challenge_method: Database["auth"]["Enums"]["code_challenge_method"]
created_at?: string | null
id: string
provider_access_token?: string | null
provider_refresh_token?: string | null
provider_type: string
updated_at?: string | null
user_id?: string | null
}
Update: {
auth_code?: string
authentication_method?: string
code_challenge?: string
code_challenge_method?: Database["auth"]["Enums"]["code_challenge_method"]
created_at?: string | null
id?: string
provider_access_token?: string | null
provider_refresh_token?: string | null
provider_type?: string
updated_at?: string | null
user_id?: string | null
}
Relationships: []
}
identities: {
Row: {
created_at: string | null
email: string | null
id: string
identity_data: Json
last_sign_in_at: string | null
provider: string
updated_at: string | null
user_id: string
}
Insert: {
created_at?: string | null
email?: string | null
id: string
identity_data: Json
last_sign_in_at?: string | null
provider: string
updated_at?: string | null
user_id: string
}
Update: {
created_at?: string | null
email?: string | null
id?: string
identity_data?: Json
last_sign_in_at?: string | null
provider?: string
updated_at?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "identities_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
instances: {
Row: {
created_at: string | null
id: string
raw_base_config: string | null
updated_at: string | null
uuid: string | null
}
Insert: {
created_at?: string | null
id: string
raw_base_config?: string | null
updated_at?: string | null
uuid?: string | null
}
Update: {
created_at?: string | null
id?: string
raw_base_config?: string | null
updated_at?: string | null
uuid?: string | null
}
Relationships: []
}
mfa_amr_claims: {
Row: {
authentication_method: string
created_at: string
id: string
session_id: string
updated_at: string
}
Insert: {
authentication_method: string
created_at: string
id: string
session_id: string
updated_at: string
}
Update: {
authentication_method?: string
created_at?: string
id?: string
session_id?: string
updated_at?: string
}
Relationships: [
{
foreignKeyName: "mfa_amr_claims_session_id_fkey"
columns: ["session_id"]
referencedRelation: "sessions"
referencedColumns: ["id"]
}
]
}
mfa_challenges: {
Row: {
created_at: string
factor_id: string
id: string
ip_address: unknown
verified_at: string | null
}
Insert: {
created_at: string
factor_id: string
id: string
ip_address: unknown
verified_at?: string | null
}
Update: {
created_at?: string
factor_id?: string
id?: string
ip_address?: unknown
verified_at?: string | null
}
Relationships: [
{
foreignKeyName: "mfa_challenges_auth_factor_id_fkey"
columns: ["factor_id"]
referencedRelation: "mfa_factors"
referencedColumns: ["id"]
}
]
}
mfa_factors: {
Row: {
created_at: string
factor_type: Database["auth"]["Enums"]["factor_type"]
friendly_name: string | null
id: string
secret: string | null
status: Database["auth"]["Enums"]["factor_status"]
updated_at: string
user_id: string
}
Insert: {
created_at: string
factor_type: Database["auth"]["Enums"]["factor_type"]
friendly_name?: string | null
id: string
secret?: string | null
status: Database["auth"]["Enums"]["factor_status"]
updated_at: string
user_id: string
}
Update: {
created_at?: string
factor_type?: Database["auth"]["Enums"]["factor_type"]
friendly_name?: string | null
id?: string
secret?: string | null
status?: Database["auth"]["Enums"]["factor_status"]
updated_at?: string
user_id?: string
}
Relationships: [
{
foreignKeyName: "mfa_factors_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
refresh_tokens: {
Row: {
created_at: string | null
id: number
instance_id: string | null
parent: string | null
revoked: boolean | null
session_id: string | null
token: string | null
updated_at: string | null
user_id: string | null
}
Insert: {
created_at?: string | null
id?: number
instance_id?: string | null
parent?: string | null
revoked?: boolean | null
session_id?: string | null
token?: string | null
updated_at?: string | null
user_id?: string | null
}
Update: {
created_at?: string | null
id?: number
instance_id?: string | null
parent?: string | null
revoked?: boolean | null
session_id?: string | null
token?: string | null
updated_at?: string | null
user_id?: string | null
}
Relationships: [
{
foreignKeyName: "refresh_tokens_session_id_fkey"
columns: ["session_id"]
referencedRelation: "sessions"
referencedColumns: ["id"]
}
]
}
saml_providers: {
Row: {
attribute_mapping: Json | null
created_at: string | null
entity_id: string
id: string
metadata_url: string | null
metadata_xml: string
sso_provider_id: string
updated_at: string | null
}
Insert: {
attribute_mapping?: Json | null
created_at?: string | null
entity_id: string
id: string
metadata_url?: string | null
metadata_xml: string
sso_provider_id: string
updated_at?: string | null
}
Update: {
attribute_mapping?: Json | null
created_at?: string | null
entity_id?: string
id?: string
metadata_url?: string | null
metadata_xml?: string
sso_provider_id?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "saml_providers_sso_provider_id_fkey"
columns: ["sso_provider_id"]
referencedRelation: "sso_providers"
referencedColumns: ["id"]
}
]
}
saml_relay_states: {
Row: {
created_at: string | null
for_email: string | null
from_ip_address: unknown | null
id: string
redirect_to: string | null
request_id: string
sso_provider_id: string
updated_at: string | null
}
Insert: {
created_at?: string | null
for_email?: string | null
from_ip_address?: unknown | null
id: string
redirect_to?: string | null
request_id: string
sso_provider_id: string
updated_at?: string | null
}
Update: {
created_at?: string | null
for_email?: string | null
from_ip_address?: unknown | null
id?: string
redirect_to?: string | null
request_id?: string
sso_provider_id?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "saml_relay_states_sso_provider_id_fkey"
columns: ["sso_provider_id"]
referencedRelation: "sso_providers"
referencedColumns: ["id"]
}
]
}
schema_migrations: {
Row: {
version: string
}
Insert: {
version: string
}
Update: {
version?: string
}
Relationships: []
}
sessions: {
Row: {
aal: Database["auth"]["Enums"]["aal_level"] | null
created_at: string | null
factor_id: string | null
id: string
not_after: string | null
updated_at: string | null
user_id: string
}
Insert: {
aal?: Database["auth"]["Enums"]["aal_level"] | null
created_at?: string | null
factor_id?: string | null
id: string
not_after?: string | null
updated_at?: string | null
user_id: string
}
Update: {
aal?: Database["auth"]["Enums"]["aal_level"] | null
created_at?: string | null
factor_id?: string | null
id?: string
not_after?: string | null
updated_at?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "sessions_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
sso_domains: {
Row: {
created_at: string | null
domain: string
id: string
sso_provider_id: string
updated_at: string | null
}
Insert: {
created_at?: string | null
domain: string
id: string
sso_provider_id: string
updated_at?: string | null
}
Update: {
created_at?: string | null
domain?: string
id?: string
sso_provider_id?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "sso_domains_sso_provider_id_fkey"
columns: ["sso_provider_id"]
referencedRelation: "sso_providers"
referencedColumns: ["id"]
}
]
}
sso_providers: {
Row: {
created_at: string | null
id: string
resource_id: string | null
updated_at: string | null
}
Insert: {
created_at?: string | null
id: string
resource_id?: string | null
updated_at?: string | null
}
Update: {
created_at?: string | null
id?: string
resource_id?: string | null
updated_at?: string | null
}
Relationships: []
}
users: {
Row: {
aud: string | null
banned_until: string | null
confirmation_sent_at: string | null
confirmation_token: string | null
confirmed_at: string | null
created_at: string | null
deleted_at: string | null
email: string | null
email_change: string | null
email_change_confirm_status: number | null
email_change_sent_at: string | null
email_change_token_current: string | null
email_change_token_new: string | null
email_confirmed_at: string | null
encrypted_password: string | null
id: string
instance_id: string | null
invited_at: string | null
is_sso_user: boolean
is_super_admin: boolean | null
last_sign_in_at: string | null
phone: string | null
phone_change: string | null
phone_change_sent_at: string | null
phone_change_token: string | null
phone_confirmed_at: string | null
raw_app_meta_data: Json | null
raw_user_meta_data: Json | null
reauthentication_sent_at: string | null
reauthentication_token: string | null
recovery_sent_at: string | null
recovery_token: string | null
role: string | null
updated_at: string | null
}
Insert: {
aud?: string | null
banned_until?: string | null
confirmation_sent_at?: string | null
confirmation_token?: string | null
confirmed_at?: string | null
created_at?: string | null
deleted_at?: string | null
email?: string | null
email_change?: string | null
email_change_confirm_status?: number | null
email_change_sent_at?: string | null
email_change_token_current?: string | null
email_change_token_new?: string | null
email_confirmed_at?: string | null
encrypted_password?: string | null
id: string
instance_id?: string | null
invited_at?: string | null
is_sso_user?: boolean
is_super_admin?: boolean | null
last_sign_in_at?: string | null
phone?: string | null
phone_change?: string | null
phone_change_sent_at?: string | null
phone_change_token?: string | null
phone_confirmed_at?: string | null
raw_app_meta_data?: Json | null
raw_user_meta_data?: Json | null
reauthentication_sent_at?: string | null
reauthentication_token?: string | null
recovery_sent_at?: string | null
recovery_token?: string | null
role?: string | null
updated_at?: string | null
}
Update: {
aud?: string | null
banned_until?: string | null
confirmation_sent_at?: string | null
confirmation_token?: string | null
confirmed_at?: string | null
created_at?: string | null
deleted_at?: string | null
email?: string | null
email_change?: string | null
email_change_confirm_status?: number | null
email_change_sent_at?: string | null
email_change_token_current?: string | null
email_change_token_new?: string | null
email_confirmed_at?: string | null
encrypted_password?: string | null
id?: string
instance_id?: string | null
invited_at?: string | null
is_sso_user?: boolean
is_super_admin?: boolean | null
last_sign_in_at?: string | null
phone?: string | null
phone_change?: string | null
phone_change_sent_at?: string | null
phone_change_token?: string | null
phone_confirmed_at?: string | null
raw_app_meta_data?: Json | null
raw_user_meta_data?: Json | null
reauthentication_sent_at?: string | null
reauthentication_token?: string | null
recovery_sent_at?: string | null
recovery_token?: string | null
role?: string | null
updated_at?: string | null
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
email: {
Args: Record<PropertyKey, never>
Returns: string
}
jwt: {
Args: Record<PropertyKey, never>
Returns: Json
}
role: {
Args: Record<PropertyKey, never>
Returns: string
}
uid: {
Args: Record<PropertyKey, never>
Returns: string
}
}
Enums: {
aal_level: "aal1" | "aal2" | "aal3"
code_challenge_method: "s256" | "plain"
factor_status: "unverified" | "verified"
factor_type: "totp" | "webauthn"
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
todos: {
Row: {
id: number
inserted_at: string
is_complete: boolean | null
task: string | null
user_id: string
}
Insert: {
id?: number
inserted_at?: string
is_complete?: boolean | null
task?: string | null
user_id: string
}
Update: {
id?: number
inserted_at?: string
is_complete?: boolean | null
task?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "todos_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "buckets_owner_fkey"
columns: ["owner"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
Relationships: []
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "objects_bucketId_fkey"
columns: ["bucket_id"]
referencedRelation: "buckets"
referencedColumns: ["id"]
},
{
foreignKeyName: "objects_owner_fkey"
columns: ["owner"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: unknown
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
+145
View File
@@ -0,0 +1,145 @@
import { TriggerClient } from "@trigger.dev/sdk";
import { createExpressServer } from "@trigger.dev/express";
import { Supabase, SupabaseManagement } from "@trigger.dev/supabase";
import { OpenAI } from "@trigger.dev/openai";
import { Database } from "./supabase-types";
const supabaseManagement = new SupabaseManagement({
id: "supabase-management",
apiKey: process.env["SUPABASE_API_KEY"]!,
});
const triggers = supabaseManagement.db<Database>(process.env["SUPABASE_ID"]!);
const supabase = new Supabase({
id: "supabase",
supabaseKey: process.env["SUPABASE_SERVICE_ROLE_KEY"]!,
supabaseUrl: process.env["SUPABASE_URL"]!,
});
const openai = new OpenAI({
id: "open-ai",
apiKey: process.env["OPENAI_API_KEY"]!,
});
export const client = new TriggerClient({
id: "job-catalog",
apiKey: process.env["TRIGGER_API_KEY"],
apiUrl: process.env["TRIGGER_API_URL"],
verbose: false,
ioLogLocalEnabled: true,
});
createExpressServer(client);
client.defineJob({
id: "supabase-management-example-1",
name: "Supabase Management Example 1",
version: "0.1.0",
trigger: triggers.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {},
});
client.defineJob({
id: "supabase-management-example-2",
name: "Supabase Management Example 2",
version: "0.1.0",
trigger: triggers.onUpdated({
table: "todos",
}),
run: async (payload, io, ctx) => {},
});
client.defineJob({
id: "supabase-management-example-users-auth",
name: "Supabase Management Example Users Auth",
version: "0.1.0",
trigger: triggers.onInserted({
table: "users",
schema: "auth",
}),
run: async (payload, io, ctx) => {},
});
client.defineJob({
id: "supabase-management-example-objects-storage",
name: "Supabase Management Example Object Storage",
version: "0.1.0",
trigger: triggers.onInserted({
schema: "storage",
table: "objects",
filter: {
record: {
bucket_id: ["example_bucket"],
name: [
{
$endsWith: ".png",
},
],
path_tokens: [
{
$includes: "images",
},
],
},
},
}),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
if (!payload.record.name) {
return;
}
const {
data: { publicUrl },
} = supabase.native.storage.from("example_bucket").getPublicUrl(payload.record.name);
const imageVariation = await io.openai.createImageVariation("variation-image", {
image: publicUrl,
n: 2,
response_format: "url",
size: "512x512",
});
const imageEdit = await io.openai.createImageEdit("edit-image", {
image: publicUrl,
prompt:
"Fill in the background to make it seem like the cat is on the moon with a beautiful view of the earth.",
n: 2,
response_format: "url",
size: "512x512",
});
// return imageEdit;
},
});
client.defineJob({
id: "supabase-management-example-on",
name: "Supabase Management Example On",
version: "0.1.0",
trigger: triggers.on({
table: "todos",
events: ["INSERT", "UPDATE"],
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {
const user = await io.supabase.runTask("fetch-user", async (db) => {
const { data, error } = await db.auth.admin.getUserById(payload.record.user_id);
if (error) {
throw error;
}
return data.user;
});
return user;
},
});
+3
View File
@@ -0,0 +1,3 @@
TRIGGER_API_KEY=tr_dev_test-api-key
TRIGGER_API_URL=http://localhost:3030
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
+35
View File
@@ -0,0 +1,35 @@
# 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
+34
View File
@@ -0,0 +1,34 @@
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.js`. 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.
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
+4
View File
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig

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