Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 321378db4f | |||
| c8b10ce188 | |||
| 57ed2b2d0d | |||
| fa3a22eb78 | |||
| a1bc15d18b | |||
| 3f48dd0a68 | |||
| 5c71868a73 | |||
| 3ce6decef4 | |||
| ab007f2708 | |||
| 8412680863 | |||
| a90908df6e | |||
| a82b86210f | |||
| 0367a92731 | |||
| d230cf622f | |||
| a1bf073b43 | |||
| 5588108b97 | |||
| b9bcecffe3 | |||
| 59075f5fc2 | |||
| fa942fc9f5 | |||
| 47a8a557cf | |||
| b51d0b5399 | |||
| 763c84e3b3 | |||
| dfb00e84e9 | |||
| b5a64545bb | |||
| 3cf3eaff48 | |||
| 4ca758a3de | |||
| da81537009 | |||
| 7c1b13ba3b | |||
| 4b654e5b3b | |||
| 726a50ace7 | |||
| 9deffb67c4 | |||
| ff04bf44ee | |||
| e740297829 | |||
| a31705e198 | |||
| f0bdf53364 | |||
| 9638499163 | |||
| 9104c252b7 | |||
| 72cf345602 | |||
| d5b8f8299d | |||
| dd22b88d25 | |||
| 62b9c5879c | |||
| 1b0973fbc1 | |||
| fc78854ed4 | |||
| ed15bb0618 | |||
| ced9034428 | |||
| 7ec329d5d6 | |||
| 81180999de | |||
| feaa79ecf3 | |||
| d34dc0847c | |||
| 1b7c7520b4 | |||
| 34d77e830c | |||
| 87c302bf26 | |||
| d462c901f5 | |||
| 55c3d79cc8 | |||
| ce901c92e4 |
@@ -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 }}
|
||||
|
||||
@@ -48,4 +48,7 @@ apps/**/public/build
|
||||
.sentryclirc
|
||||
.buildt
|
||||
|
||||
**/tmp/
|
||||
**/tmp/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
@@ -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 don’t 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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
```typescript Wait example
|
||||
new Job(client, {
|
||||
client.defineJob({
|
||||
id: "delay-job",
|
||||
name: "Delay Job",
|
||||
version: "0.0.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
```typescript
|
||||
new Job(client, {
|
||||
client.defineJob({
|
||||
//... other options
|
||||
integrations: {
|
||||
slack,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
<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:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||

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

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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) |
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 60 KiB |
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
TRIGGER_API_KEY=tr_dev_test-api-key
|
||||
TRIGGER_API_URL=http://localhost:3030
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||