Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9947383f5a | |||
| 46e1a1493b | |||
| b1cdbf52d6 | |||
| 1491d4251d | |||
| 62ac5dd137 | |||
| 23c93a5712 | |||
| 208907e10e | |||
| 3ef588e4e5 | |||
| 10836735be | |||
| 090bc2e7d2 | |||
| 4fe2fd63de | |||
| 145fb24e4d | |||
| 2382d4a5c5 | |||
| adbcfe6f20 | |||
| 0754e99507 | |||
| dfb0a79abc | |||
| c808e40f61 | |||
| a9bb53b529 | |||
| 2636d9091e | |||
| 878da3c01f | |||
| 588461188f | |||
| d4145de9a7 | |||
| 188c4b0e24 | |||
| ecd050bece | |||
| b24aeea592 | |||
| 30ba73c4f1 | |||
| bf6a2a0319 | |||
| 4fc5de0dac | |||
| 11ff63a9d2 | |||
| 0d1bdac8ab | |||
| 9f076631ae | |||
| a0d663c0fb | |||
| 8fd68e30e9 | |||
| 5c13512091 |
@@ -20,5 +20,8 @@
|
||||
"webapp",
|
||||
"emails",
|
||||
"@trigger.dev/database"
|
||||
]
|
||||
],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- ".github/workflows/release.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "changesets/**"
|
||||
- ".changeset/**"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
|
||||
Vendored
+7
-16
@@ -5,29 +5,20 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "pnpm run dev --filter webapp",
|
||||
"name": "Run webapp",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
"request": "launch",
|
||||
"name": "Debug WebApp",
|
||||
"command": "pnpm run dev --filter webapp",
|
||||
"envFile": "${workspaceFolder}/apps/webapp/.env",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Chrome webapp",
|
||||
"url": "http://localhost:3000",
|
||||
"url": "http://localhost:3030",
|
||||
"webRoot": "${workspaceFolder}/apps/webapp/app"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Current Test File",
|
||||
"autoAttachChildProcesses": true,
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
||||
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||
"args": ["run", "${relativeFile}"],
|
||||
"smartStep": true,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Clipboard, ClipboardCheck } from "lucide-react";
|
||||
import type { Language, PrismTheme } from "prism-react-renderer";
|
||||
import Highlight, { defaultProps } from "prism-react-renderer";
|
||||
import { forwardRef, useCallback, useState } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { ClipboardDocumentCheckIcon, ClipboardIcon } from "@heroicons/react/24/solid";
|
||||
import { Clipboard, ClipboardCheck, ClipboardCheckIcon } from "lucide-react";
|
||||
|
||||
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
|
||||
//it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting
|
||||
@@ -192,6 +191,9 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
);
|
||||
|
||||
// if there are more than 1000 lines, don't highlight
|
||||
const shouldHighlight = lineCount <= 1000;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative overflow-hidden rounded-md border border-slate-800", className)}
|
||||
@@ -229,99 +231,113 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<Highlight {...defaultProps} theme={theme} code={code} language={language}>
|
||||
{({
|
||||
className: inheritedClassName,
|
||||
style: inheritedStyle,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
className={cn(
|
||||
"relative mr-2 font-mono text-xs leading-relaxed",
|
||||
inheritedClassName
|
||||
)}
|
||||
style={inheritedStyle}
|
||||
{shouldHighlight ? (
|
||||
<Highlight {...defaultProps} theme={theme} code={code} language={language}>
|
||||
{({
|
||||
className: inheritedClassName,
|
||||
style: inheritedStyle,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
{tokens
|
||||
.map((line, index) => {
|
||||
if (
|
||||
index === tokens.length - 1 &&
|
||||
line.length === 1 &&
|
||||
line[0].content === "\n"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
<pre
|
||||
className={cn(
|
||||
"relative mr-2 font-mono text-xs leading-relaxed",
|
||||
inheritedClassName
|
||||
)}
|
||||
style={inheritedStyle}
|
||||
dir="ltr"
|
||||
>
|
||||
{tokens
|
||||
.map((line, index) => {
|
||||
if (
|
||||
index === tokens.length - 1 &&
|
||||
line.length === 1 &&
|
||||
line[0].content === "\n"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineNumber = index + 1;
|
||||
const lineProps = getLineProps({ line, key: index });
|
||||
const lineNumber = index + 1;
|
||||
const lineProps = getLineProps({ line, key: index });
|
||||
|
||||
let hasAnyHighlights = highlightLines ? highlightLines.length > 0 : false;
|
||||
let hasAnyHighlights = highlightLines ? highlightLines.length > 0 : false;
|
||||
|
||||
let shouldDim = hasAnyHighlights;
|
||||
if (hasAnyHighlights && highlightLines?.includes(lineNumber)) {
|
||||
shouldDim = false;
|
||||
}
|
||||
let shouldDim = hasAnyHighlights;
|
||||
if (hasAnyHighlights && highlightLines?.includes(lineNumber)) {
|
||||
shouldDim = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lineProps.key}
|
||||
{...lineProps}
|
||||
className={cn(
|
||||
"flex w-full justify-start transition-opacity duration-500",
|
||||
lineProps.className
|
||||
)}
|
||||
style={{
|
||||
opacity: shouldDim ? dimAmount : undefined,
|
||||
...lineProps.style,
|
||||
}}
|
||||
>
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className={
|
||||
"mr-2 flex-none select-none text-right text-slate-500 transition-opacity duration-500"
|
||||
}
|
||||
style={{
|
||||
width: `calc(8 * ${maxLineWidth / 16}rem)`,
|
||||
}}
|
||||
>
|
||||
{lineNumber}
|
||||
return (
|
||||
<div
|
||||
key={lineProps.key}
|
||||
{...lineProps}
|
||||
className={cn(
|
||||
"flex w-full justify-start transition-opacity duration-500",
|
||||
lineProps.className
|
||||
)}
|
||||
style={{
|
||||
opacity: shouldDim ? dimAmount : undefined,
|
||||
...lineProps.style,
|
||||
}}
|
||||
>
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className={
|
||||
"mr-2 flex-none select-none text-right text-slate-500 transition-opacity duration-500"
|
||||
}
|
||||
style={{
|
||||
width: `calc(8 * ${maxLineWidth / 16}rem)`,
|
||||
}}
|
||||
>
|
||||
{lineNumber}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
{line.map((token, key) => {
|
||||
const tokenProps = getTokenProps({ token, key });
|
||||
return (
|
||||
<span
|
||||
key={tokenProps.key}
|
||||
{...tokenProps}
|
||||
style={{
|
||||
color: tokenProps?.style?.color as string,
|
||||
...tokenProps.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
{line.map((token, key) => {
|
||||
const tokenProps = getTokenProps({ token, key });
|
||||
return (
|
||||
<span
|
||||
key={tokenProps.key}
|
||||
{...tokenProps}
|
||||
style={{
|
||||
color: tokenProps?.style?.color as string,
|
||||
...tokenProps.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="w-4 flex-none" />
|
||||
</div>
|
||||
<div className="w-4 flex-none" />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Highlight>
|
||||
) : (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<pre className="relative mr-2 p-2 font-mono text-xs leading-relaxed" dir="ltr">
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function RunCompletedDetail({ run }: { run: MatchedRun }) {
|
||||
<RunPanelDivider />
|
||||
{run.error && <RunPanelError text={run.error.message} stackTrace={run.error.stack} />}
|
||||
{run.output ? (
|
||||
<CodeBlock language="json" code={run.output} />
|
||||
<CodeBlock language="json" code={run.output} maxLines={36} />
|
||||
) : (
|
||||
run.output === null && <Paragraph variant="small">This run returned nothing</Paragraph>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DetailedTask } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
@@ -29,22 +28,16 @@ import {
|
||||
} from "../primitives/Table";
|
||||
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
import { ClientOnly } from "remix-utils";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route";
|
||||
|
||||
export function TaskDetail({ task }: { task: DetailedTask }) {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
startedAt,
|
||||
completedAt,
|
||||
status,
|
||||
delayUntil,
|
||||
params,
|
||||
properties,
|
||||
output,
|
||||
style,
|
||||
attempts,
|
||||
} = task;
|
||||
const { name, description, icon, status, params, properties, output, style, attempts } = task;
|
||||
|
||||
const startedAt = task.startedAt ? new Date(task.startedAt) : undefined;
|
||||
const completedAt = task.completedAt ? new Date(task.completedAt) : undefined;
|
||||
const delayUntil = task.delayUntil ? new Date(task.delayUntil) : undefined;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
@@ -150,7 +143,9 @@ export function TaskDetail({ task }: { task: DetailedTask }) {
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Output</Header3>
|
||||
{output ? (
|
||||
<CodeBlock code={JSON.stringify(output, null, 2)} />
|
||||
<ClientOnly fallback={<Spinner />}>
|
||||
{() => <CodeBlock code={output} maxLines={35} />}
|
||||
</ClientOnly>
|
||||
) : (
|
||||
<Paragraph variant="small">No output</Paragraph>
|
||||
)}
|
||||
|
||||
@@ -28,6 +28,7 @@ export type EnqueueRunExecutionV2Options = {
|
||||
resumeTaskId?: string;
|
||||
isRetry?: boolean;
|
||||
skipRetrying?: boolean;
|
||||
executionCount?: number;
|
||||
};
|
||||
|
||||
export async function enqueueRunExecutionV2(
|
||||
@@ -44,10 +45,11 @@ export async function enqueueRunExecutionV2(
|
||||
isRetry: typeof options.isRetry === "boolean" ? options.isRetry : false,
|
||||
},
|
||||
{
|
||||
queueName: `job:${run.jobId}:env:${run.environmentId}`,
|
||||
tx,
|
||||
runAt: options.runAt,
|
||||
jobKey: `job_run:${run.id}`,
|
||||
jobKey: `job_run:${run.id}:${options.executionCount ?? 0}${
|
||||
options.resumeTaskId ? `:task:${options.resumeTaskId}` : ""
|
||||
}`,
|
||||
maxAttempts: options.skipRetrying ? 1 : undefined,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ExternalAccount, Integration, TriggerSource } from "@trigger.dev/database";
|
||||
import { ConnectionAuth } from "@trigger.dev/core";
|
||||
import { PrismaClientOrTransaction } from "~/db.server";
|
||||
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
type ResolvableTriggerSource = TriggerSource & {
|
||||
integration: Integration;
|
||||
externalAccount: ExternalAccount | null;
|
||||
};
|
||||
|
||||
export async function resolveSourceConnection(
|
||||
tx: PrismaClientOrTransaction,
|
||||
source: ResolvableTriggerSource
|
||||
): Promise<ConnectionAuth | undefined> {
|
||||
if (source.integration.authSource !== "HOSTED") return;
|
||||
|
||||
const connection = await getConnection(tx, source);
|
||||
|
||||
if (!connection) {
|
||||
logger.error(
|
||||
`Integration connection not found for source ${source.id}, integration ${source.integration.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await integrationAuthRepository.getCredentials(connection);
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth2",
|
||||
scopes: response.scopes,
|
||||
accessToken: response.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
function getConnection(tx: PrismaClientOrTransaction, source: ResolvableTriggerSource) {
|
||||
if (source.externalAccount) {
|
||||
return tx.integrationConnection.findFirst({
|
||||
where: {
|
||||
integrationId: source.integration.id,
|
||||
externalAccountId: source.externalAccount.id,
|
||||
},
|
||||
include: {
|
||||
dataReference: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tx.integrationConnection.findFirst({
|
||||
where: {
|
||||
integrationId: source.integration.id,
|
||||
},
|
||||
include: {
|
||||
dataReference: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DisplayPropertiesSchema, StyleSchema } from "@trigger.dev/core";
|
||||
import { StyleSchema } from "@trigger.dev/core";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
|
||||
@@ -7,8 +7,6 @@ type DetailsProps = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type DetailedTask = NonNullable<Awaited<ReturnType<TaskDetailsPresenter["call"]>>>;
|
||||
|
||||
export class TaskDetailsPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -87,6 +85,7 @@ export class TaskDetailsPresenter {
|
||||
|
||||
return {
|
||||
...task,
|
||||
output: task.output ? JSON.stringify(task.output, null, 2) : undefined,
|
||||
connection: task.runConnection,
|
||||
params: task.params as Record<string, any>,
|
||||
properties: mergeProperties(task.properties, task.outputProperties),
|
||||
|
||||
@@ -62,6 +62,22 @@ export class TriggerSourcePresenter {
|
||||
},
|
||||
},
|
||||
},
|
||||
dynamicTrigger: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
sourceRegistrationJob: {
|
||||
select: {
|
||||
job: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: triggerSourceId,
|
||||
@@ -73,10 +89,15 @@ export class TriggerSourcePresenter {
|
||||
}
|
||||
|
||||
const runListPresenter = new RunListPresenter(this.#prismaClient);
|
||||
const runList = trigger.sourceRegistrationJob
|
||||
const jobSlug = getJobSlug(
|
||||
trigger.sourceRegistrationJob?.job.slug,
|
||||
trigger.dynamicTrigger?.sourceRegistrationJob?.job.slug
|
||||
);
|
||||
|
||||
const runList = jobSlug
|
||||
? await runListPresenter.call({
|
||||
userId,
|
||||
jobSlug: trigger.sourceRegistrationJob.job.slug,
|
||||
jobSlug,
|
||||
organizationSlug,
|
||||
projectSlug,
|
||||
direction,
|
||||
@@ -95,7 +116,21 @@ export class TriggerSourcePresenter {
|
||||
params: trigger.params,
|
||||
registrationJob: trigger.sourceRegistrationJob?.job,
|
||||
runList,
|
||||
dynamic: trigger.dynamicTrigger
|
||||
? { id: trigger.dynamicTrigger.id, slug: trigger.dynamicTrigger.slug }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getJobSlug(
|
||||
sourceRegistrationJobSlug: string | undefined,
|
||||
dynamicSourceRegistrationJobSlug: string | undefined
|
||||
) {
|
||||
if (sourceRegistrationJobSlug) {
|
||||
return sourceRegistrationJobSlug;
|
||||
}
|
||||
|
||||
return dynamicSourceRegistrationJobSlug;
|
||||
}
|
||||
|
||||
+18
-13
@@ -1,5 +1,7 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Await, useLoaderData } from "@remix-run/react";
|
||||
import { LoaderArgs, SerializeFrom, defer } from "@remix-run/server-runtime";
|
||||
import { Suspense } from "react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { TaskDetail } from "~/components/run/TaskDetail";
|
||||
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
@@ -10,23 +12,26 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const { taskParam } = TaskParamsSchema.parse(params);
|
||||
|
||||
const presenter = new TaskDetailsPresenter();
|
||||
const task = await presenter.call({
|
||||
const taskPromise = presenter.call({
|
||||
userId,
|
||||
id: taskParam,
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new Response(null, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
task,
|
||||
return defer({
|
||||
taskPromise,
|
||||
});
|
||||
};
|
||||
|
||||
export type DetailedTask = NonNullable<Awaited<SerializeFrom<typeof loader>["taskPromise"]>>;
|
||||
|
||||
export default function Page() {
|
||||
const { task } = useTypedLoaderData<typeof loader>();
|
||||
return <TaskDetail task={task} />;
|
||||
const { taskPromise } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Await resolve={taskPromise} errorElement={<p>Error loading task!</p>}>
|
||||
{(resolvedTask) => resolvedTask && <TaskDetail task={resolvedTask as any} />}
|
||||
</Await>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-2
@@ -43,6 +43,7 @@ import { parse } from "@conform-to/zod";
|
||||
import { z } from "zod";
|
||||
import { ActivateSourceService } from "~/services/sources/activateSource.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const user = await requireUser(request);
|
||||
@@ -90,7 +91,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
try {
|
||||
const service = new ActivateSourceService();
|
||||
|
||||
const result = await service.call(triggerParam, submission.value.jobId);
|
||||
const result = await service.call(triggerParam);
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
externalTriggerPath({ slug: organizationSlug }, { slug: projectParam }, { id: triggerParam }),
|
||||
@@ -167,6 +168,17 @@ export default function Page() {
|
||||
<NamedIcon name={trigger.active ? "active" : "inactive"} className="h-4 w-4" />
|
||||
}
|
||||
/>
|
||||
{trigger.dynamic && (
|
||||
<PageInfoProperty
|
||||
label="Dynamic"
|
||||
value={
|
||||
<span className="flex items-center gap-0.5">
|
||||
<NamedIcon name="dynamic" className="h-4 w-4" />
|
||||
{trigger.dynamic.slug}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<PageInfoProperty
|
||||
label="Environment"
|
||||
value={<EnvironmentLabel environment={trigger.environment} />}
|
||||
@@ -206,7 +218,7 @@ export default function Page() {
|
||||
</Button>
|
||||
</Callout>
|
||||
</Form>
|
||||
) : (
|
||||
) : trigger.dynamic ? null : (
|
||||
<Callout variant="error" className="justiy-between mb-4 items-center">
|
||||
This External Trigger hasn't registered successfully. Contact support for help:{" "}
|
||||
{trigger.id}
|
||||
|
||||
+16
-13
@@ -1,5 +1,7 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Await, useLoaderData } from "@remix-run/react";
|
||||
import { LoaderArgs, defer } from "@remix-run/server-runtime";
|
||||
import { Suspense } from "react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { TaskDetail } from "~/components/run/TaskDetail";
|
||||
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
@@ -10,23 +12,24 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const { taskParam } = TriggerSourceRunTaskParamsSchema.parse(params);
|
||||
|
||||
const presenter = new TaskDetailsPresenter();
|
||||
const task = await presenter.call({
|
||||
const taskPromise = presenter.call({
|
||||
userId,
|
||||
id: taskParam,
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new Response(null, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
task,
|
||||
return defer({
|
||||
taskPromise,
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { task } = useTypedLoaderData<typeof loader>();
|
||||
return <TaskDetail task={task} />;
|
||||
const { taskPromise } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Await resolve={taskPromise} errorElement={<p>Error loading task!</p>}>
|
||||
{(resolvedTask) => resolvedTask && <TaskDetail task={resolvedTask as any} />}
|
||||
</Await>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { UpdateTriggerSourceBodySchema } from "@trigger.dev/core";
|
||||
import { UpdateTriggerSourceBodyV1Schema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { UpdateSourceService } from "~/services/sources/updateSource.server";
|
||||
import { UpdateSourceServiceV1 } from "~/services/sources/updateSourceV1.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
endpointSlug: z.string(),
|
||||
@@ -40,13 +40,13 @@ export async function action({ request, params }: ActionArgs) {
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = UpdateTriggerSourceBodySchema.safeParse(anyBody);
|
||||
const body = UpdateTriggerSourceBodyV1Schema.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new UpdateSourceService();
|
||||
const service = new UpdateSourceServiceV1();
|
||||
|
||||
try {
|
||||
const source = await service.call({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { RegisterTriggerBodySchema } from "@trigger.dev/core";
|
||||
import { RegisterTriggerBodySchemaV1 } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { RegisterTriggerSourceService } from "~/services/triggers/registerTriggerSource.server";
|
||||
import { RegisterTriggerSourceServiceV1 } from "~/services/triggers/registerTriggerSourceV1.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
endpointSlug: z.string(),
|
||||
@@ -41,13 +41,13 @@ export async function action({ request, params }: ActionArgs) {
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = RegisterTriggerBodySchema.safeParse(anyBody);
|
||||
const body = RegisterTriggerBodySchemaV1.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new RegisterTriggerSourceService();
|
||||
const service = new RegisterTriggerSourceServiceV1();
|
||||
|
||||
try {
|
||||
const registration = await service.call({
|
||||
|
||||
@@ -183,7 +183,7 @@ export class RunTaskService {
|
||||
},
|
||||
},
|
||||
parent: taskBody.parentId ? { connect: { id: taskBody.parentId } } : undefined,
|
||||
name: taskBody.name,
|
||||
name: taskBody.name ?? "Task",
|
||||
description: taskBody.description,
|
||||
status,
|
||||
startedAt: new Date(),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { UpdateTriggerSourceBodyV2Schema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { UpdateSourceServiceV2 } from "~/services/sources/updateSourceV2.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
endpointSlug: z.string(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
logger.info("Updating source", { url: request.url });
|
||||
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "PUT") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const parsedParams = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
logger.info("Invalid params", { params });
|
||||
|
||||
return json({ error: "Invalid params" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
logger.info("Invalid or missing api key", { url: request.url });
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const authenticatedEnv = authenticationResult.environment;
|
||||
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = UpdateTriggerSourceBodyV2Schema.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new UpdateSourceServiceV2();
|
||||
|
||||
try {
|
||||
const source = await service.call({
|
||||
environment: authenticatedEnv,
|
||||
payload: body.data,
|
||||
endpointSlug: parsedParams.data.endpointSlug,
|
||||
id: parsedParams.data.id,
|
||||
});
|
||||
|
||||
return json(source);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error activating http source", {
|
||||
url: request.url,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
REGISTER_SOURCE_EVENT_V2,
|
||||
RegisterSourceEventV2,
|
||||
RegisterTriggerBodySchemaV2,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { RegisterTriggerSourceServiceV2 } from "~/services/triggers/registerTriggerSourceV2.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
endpointSlug: z.string(),
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
logger.info("Registering trigger", { url: request.url });
|
||||
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "PUT") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const parsedParams = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
logger.info("Invalid params", { params });
|
||||
|
||||
return json({ error: "Invalid params" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
logger.info("Invalid or missing api key", { url: request.url });
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const authenticatedEnv = authenticationResult.environment;
|
||||
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = RegisterTriggerBodySchemaV2.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new RegisterTriggerSourceServiceV2();
|
||||
|
||||
try {
|
||||
const registration = await service.call({
|
||||
environment: authenticatedEnv,
|
||||
payload: body.data,
|
||||
endpointSlug: parsedParams.data.endpointSlug,
|
||||
id: parsedParams.data.id,
|
||||
key: parsedParams.data.key,
|
||||
});
|
||||
|
||||
if (!registration) {
|
||||
return json({ error: "Could not register trigger" }, { status: 500 });
|
||||
}
|
||||
|
||||
//the source is already active
|
||||
if (registration.source.active) {
|
||||
return json(registration);
|
||||
}
|
||||
|
||||
const payload: RegisterSourceEventV2 = {
|
||||
...registration,
|
||||
dynamicTriggerId: parsedParams.data.id,
|
||||
};
|
||||
|
||||
const ingestEventService = new IngestSendEvent();
|
||||
await ingestEventService.call(
|
||||
authenticatedEnv,
|
||||
{
|
||||
id: registration.id,
|
||||
name: REGISTER_SOURCE_EVENT_V2,
|
||||
source: "trigger.dev",
|
||||
payload,
|
||||
}
|
||||
//todo accountId?
|
||||
// {accountId: body.data.accountId}
|
||||
);
|
||||
|
||||
return json(registration);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error registering trigger", {
|
||||
url: request.url,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,15 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
|
||||
try {
|
||||
const url = new URL("/", `http://${host}`);
|
||||
|
||||
if (request.headers.get("x-forwarded-proto") === "https") {
|
||||
url.protocol = "https:";
|
||||
}
|
||||
// if we can connect to the database and make a simple query
|
||||
// and make a HEAD request to ourselves, then we're good.
|
||||
await Promise.all([
|
||||
prisma.user.count(),
|
||||
fetch(url.toString(), { method: "HEAD" }).then((r) => {
|
||||
fetch(url.href, { method: "HEAD" }).then((r) => {
|
||||
if (!r.ok) return Promise.reject(r);
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ApiEventLog,
|
||||
DeliverEventResponseSchema,
|
||||
DeserializedJson,
|
||||
ErrorWithStackSchema,
|
||||
HttpSourceRequest,
|
||||
HttpSourceResponseSchema,
|
||||
@@ -9,8 +10,8 @@ import {
|
||||
PongResponseSchema,
|
||||
PreprocessRunBody,
|
||||
PreprocessRunResponseSchema,
|
||||
RegisterTriggerBody,
|
||||
RegisterTriggerBodySchema,
|
||||
RegisterTriggerBodySchemaV1,
|
||||
RegisterTriggerBodyV1,
|
||||
RunJobBody,
|
||||
RunJobResponseSchema,
|
||||
ValidateResponse,
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
} from "@trigger.dev/core";
|
||||
import { safeBodyFromResponse, safeParseBodyFromResponse } from "~/utils/json";
|
||||
import { logger } from "./logger.server";
|
||||
import { ConnectionAuth } from "@trigger.dev/core";
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
export class EndpointApiError extends Error {
|
||||
constructor(message: string, stack?: string) {
|
||||
@@ -28,10 +31,7 @@ export class EndpointApiError extends Error {
|
||||
}
|
||||
|
||||
export class EndpointApi {
|
||||
constructor(
|
||||
private apiKey: string,
|
||||
private url: string
|
||||
) {}
|
||||
constructor(private apiKey: string, private url: string) {}
|
||||
|
||||
async ping(endpointId: string): Promise<PongResponse> {
|
||||
const response = await safeFetch(this.url, {
|
||||
@@ -165,9 +165,7 @@ export class EndpointApi {
|
||||
}
|
||||
|
||||
async executeJobRequest(options: RunJobBody) {
|
||||
logger.debug("executeJobRequest()", {
|
||||
options,
|
||||
});
|
||||
const startTimeInMs = performance.now();
|
||||
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
@@ -183,6 +181,7 @@ export class EndpointApi {
|
||||
response,
|
||||
parser: RunJobResponseSchema,
|
||||
errorParser: ErrorWithStackSchema,
|
||||
durationInMs: Math.floor(performance.now() - startTimeInMs),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@ export class EndpointApi {
|
||||
return { response, parser: PreprocessRunResponseSchema };
|
||||
}
|
||||
|
||||
async initializeTrigger(id: string, params: any): Promise<RegisterTriggerBody | undefined> {
|
||||
async initializeTrigger(id: string, params: any): Promise<RegisterTriggerBodyV1 | undefined> {
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -234,7 +233,7 @@ export class EndpointApi {
|
||||
body: anyBody,
|
||||
});
|
||||
|
||||
return RegisterTriggerBodySchema.parse(anyBody);
|
||||
return RegisterTriggerBodySchemaV1.parse(anyBody);
|
||||
}
|
||||
|
||||
async deliverHttpSourceRequest(options: {
|
||||
@@ -244,6 +243,8 @@ export class EndpointApi {
|
||||
params: any;
|
||||
data: any;
|
||||
request: HttpSourceRequest;
|
||||
auth?: ConnectionAuth;
|
||||
metadata?: any;
|
||||
}) {
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
@@ -258,7 +259,9 @@ export class EndpointApi {
|
||||
"x-ts-http-url": options.request.url,
|
||||
"x-ts-http-method": options.request.method,
|
||||
"x-ts-http-headers": JSON.stringify(options.request.headers),
|
||||
...(options.auth && { "x-ts-auth": JSON.stringify(options.auth) }),
|
||||
...(options.dynamicId && { "x-ts-dynamic-id": options.dynamicId }),
|
||||
...(options.metadata && { "x-ts-metadata": JSON.stringify(options.metadata) }),
|
||||
},
|
||||
body: options.request.rawBody,
|
||||
});
|
||||
|
||||
@@ -4,16 +4,18 @@ import { findEndpoint } from "~/models/endpoint.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterJobService } from "../jobs/registerJob.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { RegisterSourceService } from "../sources/registerSource.server";
|
||||
import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server";
|
||||
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
|
||||
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
|
||||
import { DisableJobService } from "../jobs/disableJob.server";
|
||||
import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server";
|
||||
|
||||
export class IndexEndpointService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerJobService = new RegisterJobService();
|
||||
#disableJobService = new DisableJobService();
|
||||
#registerSourceService = new RegisterSourceService();
|
||||
#registerSourceServiceV1 = new RegisterSourceServiceV1();
|
||||
#registerSourceServiceV2 = new RegisterSourceServiceV2();
|
||||
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
|
||||
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
|
||||
|
||||
@@ -156,7 +158,17 @@ export class IndexEndpointService {
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
await this.#registerSourceService.call(endpoint, source);
|
||||
switch (source.version) {
|
||||
default:
|
||||
case "1": {
|
||||
await this.#registerSourceServiceV1.call(endpoint, source);
|
||||
break;
|
||||
}
|
||||
case "2": {
|
||||
await this.#registerSourceServiceV2.call(endpoint, source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
indexStats.sources++;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { airtable } from "./integrations/airtable";
|
||||
import { github } from "./integrations/github";
|
||||
import { openai } from "./integrations/openai";
|
||||
import { plain } from "./integrations/plain";
|
||||
import { resend } from "./integrations/resend";
|
||||
import { sendgrid } from "./integrations/sendgrid";
|
||||
import { slack } from "./integrations/slack";
|
||||
import { stripe } from "./integrations/stripe";
|
||||
import { sendgrid } from "./integrations/sendgrid";
|
||||
import { supabaseManagement, supabase } from "./integrations/supabase";
|
||||
import { supabase, supabaseManagement } from "./integrations/supabase";
|
||||
import { typeform } from "./integrations/typeform";
|
||||
import type { Integration } from "./types";
|
||||
|
||||
@@ -30,6 +31,7 @@ export class IntegrationCatalog {
|
||||
}
|
||||
|
||||
export const integrationCatalog = new IntegrationCatalog({
|
||||
airtable,
|
||||
github,
|
||||
openai,
|
||||
plain,
|
||||
|
||||
@@ -1,4 +1,52 @@
|
||||
import type { Integration } from "../types";
|
||||
import type { HelpSample, Integration } from "../types";
|
||||
|
||||
function usageSample(hasApiKey: boolean): HelpSample {
|
||||
return {
|
||||
title: "Using the client",
|
||||
code: `
|
||||
import { Airtable } from "@trigger.dev/airtable";
|
||||
|
||||
const airtable = new Airtable({
|
||||
id: "__SLUG__"${hasApiKey ? ",\n token: process.env.AIRTABLE_TOKEN!" : ""}
|
||||
});
|
||||
|
||||
//you can define your Airtable table types
|
||||
type LaunchGoalsAndOkRs = {
|
||||
"Launch goals"?: string;
|
||||
DRI?: Collaborator;
|
||||
Team?: string;
|
||||
Status?: "On track" | "In progress" | "At risk";
|
||||
"Key results"?: Array<string>;
|
||||
"Features (from 💻 Features table)"?: Array<string>;
|
||||
"Status (from 💻 Features)": Array<"Live" | "Complete" | "In progress" | "Planning" | "In reviews">;
|
||||
};
|
||||
|
||||
client.defineJob({
|
||||
id: "airtable-example-1",
|
||||
name: "Airtable Example 1: getRecords",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({
|
||||
name: "airtable.example",
|
||||
schema: z.object({
|
||||
baseId: z.string(),
|
||||
tableName: z.string(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
airtable,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
//then you can set the types for your table, so you get type safety
|
||||
const table = io.airtable.base(payload.baseId).table<LaunchGoalsAndOkRs>(payload.tableName);
|
||||
|
||||
const records = await table.getRecords("muliple records", { fields: ["Status"] });
|
||||
//this will be type checked
|
||||
await io.logger.log(records[0].fields.Status ?? "no status");
|
||||
},
|
||||
});
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
export const airtable: Integration = {
|
||||
identifier: "airtable",
|
||||
@@ -70,18 +118,13 @@ export const airtable: Integration = {
|
||||
},
|
||||
],
|
||||
help: {
|
||||
samples: [
|
||||
{
|
||||
title: "Creating the client",
|
||||
code: `
|
||||
import { Airtable } from "@trigger.dev/airtable";
|
||||
|
||||
const airtable = new Airtable({
|
||||
id: "__SLUG__"
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
samples: [usageSample(false)],
|
||||
},
|
||||
},
|
||||
apiKey: {
|
||||
type: "apikey",
|
||||
help: {
|
||||
samples: [usageSample(true)],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,6 +20,13 @@ import { logger } from "../logger.server";
|
||||
type FoundRun = NonNullable<Awaited<ReturnType<typeof findRun>>>;
|
||||
type FoundTask = FoundRun["tasks"][number];
|
||||
|
||||
export type PerformRunExecutionV2Input = {
|
||||
id: string;
|
||||
reason: "PREPROCESS" | "EXECUTE_JOB";
|
||||
isRetry: boolean;
|
||||
resumeTaskId?: string;
|
||||
};
|
||||
|
||||
export class PerformRunExecutionV2Service {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -27,25 +34,20 @@ export class PerformRunExecutionV2Service {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(
|
||||
id: string,
|
||||
reason: "PREPROCESS" | "EXECUTE_JOB",
|
||||
isRetry: boolean = false,
|
||||
resumeTaskId?: string
|
||||
) {
|
||||
const run = await findRun(this.#prismaClient, id);
|
||||
public async call(input: PerformRunExecutionV2Input) {
|
||||
const run = await findRun(this.#prismaClient, input.id);
|
||||
|
||||
if (!run) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reason) {
|
||||
switch (input.reason) {
|
||||
case "PREPROCESS": {
|
||||
await this.#executePreprocessing(run);
|
||||
break;
|
||||
}
|
||||
case "EXECUTE_JOB": {
|
||||
await this.#executeJob(run, isRetry, resumeTaskId);
|
||||
await this.#executeJob(run, input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -141,7 +143,9 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
}
|
||||
async #executeJob(run: FoundRun, isRetry: boolean, resumeTaskId?: string) {
|
||||
async #executeJob(run: FoundRun, input: PerformRunExecutionV2Input) {
|
||||
const { isRetry, resumeTaskId } = input;
|
||||
|
||||
if (run.status === "CANCELED") {
|
||||
await this.#cancelExecution(run);
|
||||
return;
|
||||
@@ -152,21 +156,27 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
const startedAt = new Date();
|
||||
|
||||
await this.#prismaClient.jobRun.update({
|
||||
const { executionCount } = await this.#prismaClient.jobRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
status: run.status === "QUEUED" ? "STARTED" : run.status,
|
||||
startedAt: run.startedAt ?? new Date(),
|
||||
executionCount: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
executionCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connections = await resolveRunConnections(run.runConnections);
|
||||
|
||||
if (!connections.success) {
|
||||
return this.#failRunExecutionWithRetry({
|
||||
message: `Could not resolve all connections for run ${run.id}, attempting to retry`,
|
||||
return this.#failRunExecution(this.#prismaClient, "EXECUTE_JOB", run, {
|
||||
message: `Could not resolve all connections for run ${run.id}. This should not happen`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,7 +205,7 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext);
|
||||
|
||||
const { response, parser, errorParser } = await client.executeJobRequest({
|
||||
const { response, parser, errorParser, durationInMs } = await client.executeJobRequest({
|
||||
event,
|
||||
job: {
|
||||
id: run.version.job.slug,
|
||||
@@ -261,50 +271,82 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
// Only retry if the error isn't a 4xx
|
||||
if (response.status >= 400 && response.status <= 499 && response.status !== 408) {
|
||||
return await this.#failRunExecution(this.#prismaClient, "EXECUTE_JOB", run, {
|
||||
message: `Endpoint responded with ${response.status} status code`,
|
||||
});
|
||||
return await this.#failRunExecution(
|
||||
this.#prismaClient,
|
||||
"EXECUTE_JOB",
|
||||
run,
|
||||
{
|
||||
message: `Endpoint responded with ${response.status} status code`,
|
||||
},
|
||||
"FAILURE",
|
||||
durationInMs
|
||||
);
|
||||
} else {
|
||||
return await this.#failRunExecutionWithRetry({
|
||||
message: `Endpoint responded with ${response.status} status code`,
|
||||
});
|
||||
// If the error is a 504 timeout, we should mark this execution as succeeded (by not throwing an error) and enqueue a new execution
|
||||
if (response.status === 504) {
|
||||
return await this.#resumeRunExecutionAfterTimeout(
|
||||
this.#prismaClient,
|
||||
run,
|
||||
input,
|
||||
durationInMs,
|
||||
executionCount
|
||||
);
|
||||
} else {
|
||||
return await this.#failRunExecutionWithRetry({
|
||||
message: `Endpoint responded with ${response.status} status code`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safeBody = safeJsonZodParse(parser, rawBody);
|
||||
|
||||
if (!safeBody) {
|
||||
return await this.#failRunExecution(this.#prismaClient, "EXECUTE_JOB", run, {
|
||||
message: "Endpoint responded with invalid JSON",
|
||||
});
|
||||
return await this.#failRunExecution(
|
||||
this.#prismaClient,
|
||||
"EXECUTE_JOB",
|
||||
run,
|
||||
{
|
||||
message: "Endpoint responded with invalid JSON",
|
||||
},
|
||||
"FAILURE",
|
||||
durationInMs
|
||||
);
|
||||
}
|
||||
|
||||
if (!safeBody.success) {
|
||||
return await this.#failRunExecution(this.#prismaClient, "EXECUTE_JOB", run, {
|
||||
message: generateErrorMessage(safeBody.error.issues),
|
||||
});
|
||||
return await this.#failRunExecution(
|
||||
this.#prismaClient,
|
||||
"EXECUTE_JOB",
|
||||
run,
|
||||
{
|
||||
message: generateErrorMessage(safeBody.error.issues),
|
||||
},
|
||||
"FAILURE",
|
||||
durationInMs
|
||||
);
|
||||
}
|
||||
|
||||
const status = safeBody.data.status;
|
||||
|
||||
switch (status) {
|
||||
case "SUCCESS": {
|
||||
await this.#completeRunWithSuccess(run, safeBody.data);
|
||||
await this.#completeRunWithSuccess(run, safeBody.data, durationInMs);
|
||||
|
||||
break;
|
||||
}
|
||||
case "RESUME_WITH_TASK": {
|
||||
await this.#resumeRunWithTask(run, safeBody.data, isRetry);
|
||||
await this.#resumeRunWithTask(run, safeBody.data, isRetry, durationInMs, executionCount);
|
||||
|
||||
break;
|
||||
}
|
||||
case "ERROR": {
|
||||
await this.#failRunWithError(run, safeBody.data);
|
||||
await this.#failRunWithError(run, safeBody.data, durationInMs);
|
||||
|
||||
break;
|
||||
}
|
||||
case "RETRY_WITH_TASK": {
|
||||
await this.#retryRunWithTask(run, safeBody.data, isRetry);
|
||||
await this.#retryRunWithTask(run, safeBody.data, isRetry, durationInMs, executionCount);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -319,19 +361,37 @@ export class PerformRunExecutionV2Service {
|
||||
}
|
||||
}
|
||||
|
||||
async #completeRunWithSuccess(run: FoundRun, data: RunJobSuccess) {
|
||||
async #completeRunWithSuccess(run: FoundRun, data: RunJobSuccess, durationInMs: number) {
|
||||
await this.#prismaClient.jobRun.update({
|
||||
where: { id: run.id },
|
||||
data: {
|
||||
completedAt: new Date(),
|
||||
status: "SUCCESS",
|
||||
output: data.output ?? undefined,
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunWithTask(run: FoundRun, data: RunJobResumeWithTask, isRetry: boolean) {
|
||||
async #resumeRunWithTask(
|
||||
run: FoundRun,
|
||||
data: RunJobResumeWithTask,
|
||||
isRetry: boolean,
|
||||
durationInMs: number,
|
||||
executionCount: number
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.jobRun.update({
|
||||
where: { id: run.id },
|
||||
data: {
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// If the task has an operation, then the next performRunExecution will occur
|
||||
// when that operation has finished
|
||||
if (!data.task.operation) {
|
||||
@@ -340,12 +400,13 @@ export class PerformRunExecutionV2Service {
|
||||
resumeTaskId: data.task.id,
|
||||
isRetry,
|
||||
skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
executionCount,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithError(execution: FoundRun, data: RunJobError) {
|
||||
async #failRunWithError(execution: FoundRun, data: RunJobError, durationInMs: number) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
if (data.task) {
|
||||
await tx.task.update({
|
||||
@@ -360,11 +421,24 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
await this.#failRunExecution(tx, "EXECUTE_JOB", execution, data.error ?? undefined);
|
||||
await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
execution,
|
||||
data.error ?? undefined,
|
||||
"FAILURE",
|
||||
durationInMs
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async #retryRunWithTask(run: FoundRun, data: RunJobRetryWithTask, isRetry: boolean) {
|
||||
async #retryRunWithTask(
|
||||
run: FoundRun,
|
||||
data: RunJobRetryWithTask,
|
||||
isRetry: boolean,
|
||||
durationInMs: number,
|
||||
executionCount: number
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
// We need to check for an existing task attempt
|
||||
const existingAttempt = await tx.taskAttempt.findFirst({
|
||||
@@ -405,6 +479,13 @@ export class PerformRunExecutionV2Service {
|
||||
},
|
||||
data: {
|
||||
status: "WAITING",
|
||||
run: {
|
||||
update: {
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -413,6 +494,55 @@ export class PerformRunExecutionV2Service {
|
||||
resumeTaskId: data.task.id,
|
||||
isRetry,
|
||||
skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
executionCount,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunExecutionAfterTimeout(
|
||||
prisma: PrismaClientOrTransaction,
|
||||
run: FoundRun,
|
||||
input: PerformRunExecutionV2Input,
|
||||
durationInMs: number,
|
||||
executionCount: number
|
||||
) {
|
||||
await $transaction(prisma, async (tx) => {
|
||||
const executionDuration = run.executionDuration + durationInMs;
|
||||
|
||||
// If the execution duration is greater than the maximum execution time, we need to fail the run
|
||||
if (executionDuration >= run.organization.maximumExecutionTimePerRunInMs) {
|
||||
await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
run,
|
||||
{
|
||||
message: `Execution timed out after ${
|
||||
run.organization.maximumExecutionTimePerRunInMs / 1000
|
||||
} seconds`,
|
||||
},
|
||||
"TIMED_OUT",
|
||||
durationInMs
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await tx.jobRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// The run has timed out, so we need to enqueue a new execution
|
||||
await enqueueRunExecutionV2(run, tx, {
|
||||
resumeTaskId: input.resumeTaskId,
|
||||
isRetry: input.isRetry,
|
||||
skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
executionCount,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -426,7 +556,8 @@ export class PerformRunExecutionV2Service {
|
||||
reason: "EXECUTE_JOB" | "PREPROCESS",
|
||||
run: FoundRun,
|
||||
output: Record<string, any>,
|
||||
status: "FAILURE" | "ABORTED" = "FAILURE"
|
||||
status: "FAILURE" | "ABORTED" | "TIMED_OUT" = "FAILURE",
|
||||
durationInMs: number = 0
|
||||
): Promise<void> {
|
||||
await $transaction(prisma, async (tx) => {
|
||||
switch (reason) {
|
||||
@@ -438,6 +569,9 @@ export class PerformRunExecutionV2Service {
|
||||
completedAt: new Date(),
|
||||
status,
|
||||
output,
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -510,38 +644,23 @@ function prepareTasksForRun(possibleTasks: FoundTask[]): CachedTask[] {
|
||||
return cachedTaskSizes.get(taskId)!;
|
||||
}
|
||||
|
||||
// Create a dynamic programming array to store intermediate results
|
||||
const dp: number[][] = [];
|
||||
for (let i = 0; i <= tasks.length; i++) {
|
||||
dp[i] = [];
|
||||
for (let j = 0; j <= TOTAL_CACHED_TASK_BYTE_LIMIT; j++) {
|
||||
dp[i][j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the dynamic programming array
|
||||
for (let i = 1; i <= tasks.length; i++) {
|
||||
const task = tasks[i - 1];
|
||||
// Prepare tasks and calculate their sizes
|
||||
const availableTasks = tasks.map((task) => {
|
||||
const cachedTask = getCachedTask(task);
|
||||
const taskSize = getCachedTaskSize(cachedTask);
|
||||
for (let j = 0; j <= TOTAL_CACHED_TASK_BYTE_LIMIT; j++) {
|
||||
if (taskSize <= j) {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - taskSize] + taskSize);
|
||||
} else {
|
||||
dp[i][j] = dp[i - 1][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return { task: cachedTask, size: getCachedTaskSize(cachedTask) };
|
||||
});
|
||||
|
||||
// Traverse the dynamic programming array to find the included tasks
|
||||
// Sort tasks in ascending order by size
|
||||
availableTasks.sort((a, b) => a.size - b.size);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let j = TOTAL_CACHED_TASK_BYTE_LIMIT;
|
||||
for (let i = tasks.length; i > 0 && j > 0; i--) {
|
||||
if (dp[i][j] !== dp[i - 1][j]) {
|
||||
const task = tasks[i - 1];
|
||||
const cachedTask = getCachedTask(task);
|
||||
tasksToRun.unshift(cachedTask);
|
||||
j -= getCachedTaskSize(cachedTask);
|
||||
let remainingSize = TOTAL_CACHED_TASK_BYTE_LIMIT;
|
||||
|
||||
for (const { task, size } of availableTasks) {
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(task);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,7 +690,6 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
endpoint: true,
|
||||
organization: true,
|
||||
externalAccount: true,
|
||||
queue: true,
|
||||
runConnections: {
|
||||
include: {
|
||||
integration: true,
|
||||
@@ -588,6 +706,14 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
in: ["COMPLETED"],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idempotencyKey: true,
|
||||
status: true,
|
||||
noop: true,
|
||||
output: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
version: {
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import {
|
||||
REGISTER_SOURCE_EVENT_V2,
|
||||
REGISTER_SOURCE_EVENT_V1,
|
||||
RegisterTriggerSource,
|
||||
RegisterSourceEventV1,
|
||||
RegisterSourceEventV2,
|
||||
RegisterSourceEventOptions,
|
||||
RegisteredOptionsDiff,
|
||||
} from "@trigger.dev/core";
|
||||
import type { SecretReference, TriggerSource, TriggerSourceOption } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import type { TriggerSource, SecretReference, TriggerSourceEvent } from "@trigger.dev/database";
|
||||
import type { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { env } from "~/env.server";
|
||||
import { REGISTER_SOURCE_EVENT, RegisterTriggerSource } from "@trigger.dev/core";
|
||||
import { SecretStoreProvider, getSecretStore } from "../secrets/secretStore.server";
|
||||
import { SecretStore } from "../secrets/secretStore.server";
|
||||
import { z } from "zod";
|
||||
import type { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { IngestSendEvent } from "../events/ingestSendEvent.server";
|
||||
import { getSecretStore } from "../secrets/secretStore.server";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export class ActivateSourceService {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -16,7 +24,7 @@ export class ActivateSourceService {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(id: string, jobId: string, orphanedEvents?: Array<string>) {
|
||||
public async call(id: string, jobId?: string, orphanedOptions?: Record<string, string[]>) {
|
||||
const triggerSource = await this.#prismaClient.triggerSource.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
@@ -29,12 +37,12 @@ export class ActivateSourceService {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
events: true,
|
||||
options: true,
|
||||
secretReference: true,
|
||||
},
|
||||
});
|
||||
|
||||
const eventId = `${id}:${jobId}`;
|
||||
const eventId = `${id}:${jobId ?? nanoid()}`;
|
||||
|
||||
// TODO: support more channels
|
||||
switch (triggerSource.channel) {
|
||||
@@ -42,10 +50,10 @@ export class ActivateSourceService {
|
||||
await this.#activateHttpSource(
|
||||
triggerSource.environment,
|
||||
triggerSource,
|
||||
triggerSource.events,
|
||||
triggerSource.options,
|
||||
triggerSource.secretReference,
|
||||
eventId,
|
||||
orphanedEvents
|
||||
orphanedOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,10 +62,10 @@ export class ActivateSourceService {
|
||||
async #activateHttpSource(
|
||||
environment: AuthenticatedEnvironment,
|
||||
triggerSource: TriggerSource,
|
||||
events: Array<TriggerSourceEvent>,
|
||||
options: Array<TriggerSourceOption>,
|
||||
secretReference: SecretReference,
|
||||
eventId: string,
|
||||
orphanedEvents?: Array<string>
|
||||
orphanedOptions?: Record<string, string[]>
|
||||
) {
|
||||
const secretStore = getSecretStore(secretReference.provider);
|
||||
const httpSecret = await secretStore.getSecret(
|
||||
@@ -71,14 +79,6 @@ export class ActivateSourceService {
|
||||
throw new Error("HTTP Secret not found");
|
||||
}
|
||||
|
||||
const eventNames = triggerSource.active
|
||||
? events.filter((e) => e.registered).map((e) => e.name)
|
||||
: events.map((e) => e.name);
|
||||
|
||||
const missingEvents = triggerSource.active
|
||||
? events.filter((e) => !e.registered).map((e) => e.name)
|
||||
: [];
|
||||
|
||||
const service = new IngestSendEvent();
|
||||
|
||||
const source: RegisterTriggerSource = {
|
||||
@@ -92,17 +92,85 @@ export class ActivateSourceService {
|
||||
},
|
||||
};
|
||||
|
||||
await service.call(environment, {
|
||||
id: eventId,
|
||||
name: REGISTER_SOURCE_EVENT,
|
||||
source: "trigger.dev",
|
||||
payload: {
|
||||
id: triggerSource.id,
|
||||
source,
|
||||
events: eventNames,
|
||||
missingEvents,
|
||||
orphanedEvents: orphanedEvents ?? [],
|
||||
},
|
||||
});
|
||||
switch (triggerSource.version) {
|
||||
case "1": {
|
||||
const events = triggerSource.active
|
||||
? options.filter((e) => e.registered).map((e) => e.value)
|
||||
: options.map((e) => e.value);
|
||||
const missingEvents = triggerSource.active
|
||||
? options.filter((e) => !e.registered).map((e) => e.value)
|
||||
: [];
|
||||
const orphanedEvents = orphanedOptions
|
||||
? Object.values(orphanedOptions).flatMap((vals) => vals)
|
||||
: [];
|
||||
|
||||
const payload: RegisterSourceEventV1 = {
|
||||
id: triggerSource.id,
|
||||
source,
|
||||
events,
|
||||
missingEvents,
|
||||
orphanedEvents,
|
||||
};
|
||||
|
||||
await service.call(environment, {
|
||||
id: eventId,
|
||||
name: REGISTER_SOURCE_EVENT_V1,
|
||||
source: "trigger.dev",
|
||||
payload,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "2": {
|
||||
//group the options by the name
|
||||
const optionsRecord = options.reduce((acc, option) => {
|
||||
if (!acc[option.name]) {
|
||||
acc[option.name] = [];
|
||||
}
|
||||
|
||||
acc[option.name].push(option);
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, Array<TriggerSourceOption>>);
|
||||
|
||||
//for each of the optionsRecord, create the diff
|
||||
const payloadOptions = Object.entries(optionsRecord).reduce(
|
||||
(acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: getOptionsDiff(triggerSource.active, orphanedOptions?.[key] ?? [], value),
|
||||
}),
|
||||
{} as Record<string, RegisteredOptionsDiff>
|
||||
) as RegisterSourceEventOptions;
|
||||
|
||||
const payload: RegisterSourceEventV2 = {
|
||||
id: triggerSource.id,
|
||||
source,
|
||||
options: payloadOptions,
|
||||
};
|
||||
|
||||
await service.call(environment, {
|
||||
id: eventId,
|
||||
name: REGISTER_SOURCE_EVENT_V2,
|
||||
source: "trigger.dev",
|
||||
payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionsDiff(
|
||||
sourceIsActive: boolean,
|
||||
orphaned: string[],
|
||||
options: Array<TriggerSourceOption>
|
||||
): RegisteredOptionsDiff {
|
||||
const desired = sourceIsActive
|
||||
? options.filter((e) => e.registered).map((e) => e.value)
|
||||
: options.map((e) => e.value);
|
||||
const missing = sourceIsActive ? options.filter((e) => !e.registered).map((e) => e.value) : [];
|
||||
|
||||
return {
|
||||
desired: [...new Set(desired)],
|
||||
missing: [...new Set(missing)],
|
||||
orphaned: [...new Set(orphaned)],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import { prisma } from "~/db.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { IngestSendEvent } from "../events/ingestSendEvent.server";
|
||||
import { getSecretStore } from "../secrets/secretStore.server";
|
||||
import { resolveApiConnection, resolveRunConnection } from "~/models/runConnection.server";
|
||||
import { ConnectionAuth } from "@trigger.dev/sdk";
|
||||
import { resolveSourceConnection } from "~/models/sourceConnection.server";
|
||||
|
||||
export class DeliverHttpSourceRequestService {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -28,7 +31,11 @@ export class DeliverHttpSourceRequestService {
|
||||
secretReference: true,
|
||||
dynamicTrigger: true,
|
||||
externalAccount: true,
|
||||
integration: true,
|
||||
integration: {
|
||||
include: {
|
||||
connections: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -51,14 +58,14 @@ export class DeliverHttpSourceRequestService {
|
||||
throw new Error(`Secret not found for ${httpSourceRequest.source.key}`);
|
||||
}
|
||||
|
||||
// TODO: implement auth for http source requests
|
||||
const auth = await resolveSourceConnection(this.#prismaClient, httpSourceRequest.source);
|
||||
|
||||
const clientApi = new EndpointApi(
|
||||
httpSourceRequest.environment.apiKey,
|
||||
httpSourceRequest.endpoint.url
|
||||
);
|
||||
|
||||
const { response, events } = await clientApi.deliverHttpSourceRequest({
|
||||
const { response, events, metadata } = await clientApi.deliverHttpSourceRequest({
|
||||
key: httpSourceRequest.source.key,
|
||||
dynamicId: httpSourceRequest.source.dynamicTrigger?.slug,
|
||||
secret: secret.secret,
|
||||
@@ -70,6 +77,8 @@ export class DeliverHttpSourceRequestService {
|
||||
headers: httpSourceRequest.headers as Record<string, string>,
|
||||
rawBody: httpSourceRequest.body,
|
||||
},
|
||||
auth,
|
||||
metadata: httpSourceRequest.source.metadata,
|
||||
});
|
||||
|
||||
await this.#prismaClient.httpSourceRequestDelivery.update({
|
||||
@@ -81,6 +90,17 @@ export class DeliverHttpSourceRequestService {
|
||||
},
|
||||
});
|
||||
|
||||
if (metadata) {
|
||||
await this.#prismaClient.triggerSource.update({
|
||||
where: {
|
||||
id: httpSourceRequest.source.id,
|
||||
},
|
||||
data: {
|
||||
metadata: metadata,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ingestService = new IngestSendEvent();
|
||||
|
||||
for (const event of events) {
|
||||
|
||||
@@ -54,7 +54,7 @@ export class HandleHttpSourceService {
|
||||
id: delivery.id,
|
||||
},
|
||||
{
|
||||
queueName: `endpoint-${triggerSource.endpointId}`,
|
||||
queueName: `deliver:${triggerSource.id}`,
|
||||
tx,
|
||||
maxAttempts:
|
||||
triggerSource.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
|
||||
|
||||
+21
-17
@@ -1,5 +1,5 @@
|
||||
import type { Endpoint } from "@trigger.dev/database";
|
||||
import type { SourceMetadata } from "@trigger.dev/core";
|
||||
import type { SourceMetadataV1 } from "@trigger.dev/core";
|
||||
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import type { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
@@ -7,7 +7,7 @@ import { workerQueue } from "../worker.server";
|
||||
import { generateSecret } from "./utils.server";
|
||||
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
|
||||
|
||||
export class RegisterSourceService {
|
||||
export class RegisterSourceServiceV1 {
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
|
||||
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
|
||||
@@ -16,7 +16,7 @@ export class RegisterSourceService {
|
||||
|
||||
public async call(
|
||||
endpointIdOrEndpoint: string | ExtendedEndpoint,
|
||||
metadata: SourceMetadata,
|
||||
metadata: SourceMetadataV1,
|
||||
dynamicTriggerId?: string,
|
||||
accountId?: string,
|
||||
dynamicSource?: { id: string; metadata: any }
|
||||
@@ -39,7 +39,7 @@ export class RegisterSourceService {
|
||||
async #upsertSource(
|
||||
endpoint: Endpoint,
|
||||
environment: AuthenticatedEnvironment,
|
||||
metadata: SourceMetadata,
|
||||
metadata: SourceMetadataV1,
|
||||
dynamicTriggerId?: string,
|
||||
accountId?: string,
|
||||
dynamicSource?: { id: string; metadata: any }
|
||||
@@ -121,9 +121,10 @@ export class RegisterSourceService {
|
||||
}
|
||||
: undefined,
|
||||
externalAccount: externalAccount ? { connect: { id: externalAccount.id } } : undefined,
|
||||
events: {
|
||||
options: {
|
||||
create: metadata.events.map((event) => ({
|
||||
name: event,
|
||||
name: "event",
|
||||
value: event,
|
||||
})),
|
||||
},
|
||||
secretReference: {
|
||||
@@ -175,7 +176,7 @@ export class RegisterSourceService {
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
events: true,
|
||||
options: true,
|
||||
secretReference: true,
|
||||
},
|
||||
});
|
||||
@@ -200,22 +201,24 @@ export class RegisterSourceService {
|
||||
const newEvents = new Set<string>(metadata.events);
|
||||
const orphanedEvents = new Set<string>();
|
||||
|
||||
for (const event of triggerSource.events) {
|
||||
if (!newEvents.has(event.name)) {
|
||||
orphanedEvents.add(event.name);
|
||||
for (const option of triggerSource.options) {
|
||||
if (!newEvents.has(option.value)) {
|
||||
orphanedEvents.add(option.value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of newEvents) {
|
||||
await tx.triggerSourceEvent.upsert({
|
||||
await tx.triggerSourceOption.upsert({
|
||||
where: {
|
||||
name_sourceId: {
|
||||
name: event,
|
||||
name_value_sourceId: {
|
||||
name: "event",
|
||||
value: event,
|
||||
sourceId: triggerSource.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
name: event,
|
||||
name: "event",
|
||||
value: event,
|
||||
source: {
|
||||
connect: {
|
||||
id: triggerSource.id,
|
||||
@@ -249,7 +252,7 @@ export class RegisterSourceService {
|
||||
id: id,
|
||||
},
|
||||
include: {
|
||||
events: true,
|
||||
options: true,
|
||||
secretReference: true,
|
||||
integration: true,
|
||||
},
|
||||
@@ -261,11 +264,12 @@ export class RegisterSourceService {
|
||||
|
||||
const triggerIsActive = triggerSource.active;
|
||||
const triggerHasOrphanedEvents = orphanedEvents.length > 0;
|
||||
const triggerHasUnregisteredEvents = triggerSource.events.some((event) => !event.registered);
|
||||
const triggerHasUnregisteredEvents = triggerSource.options.some((option) => !option.registered);
|
||||
|
||||
if (!triggerIsActive || triggerHasOrphanedEvents || triggerHasUnregisteredEvents) {
|
||||
// We need to re-activate the source, and there could be orphaned events
|
||||
await workerQueue.enqueue("activateSource", {
|
||||
version: "1",
|
||||
id: triggerSource.id,
|
||||
orphanedEvents: orphanedEvents,
|
||||
});
|
||||
@@ -277,7 +281,7 @@ export class RegisterSourceService {
|
||||
async #findOrCreateIntegration(
|
||||
tx: PrismaClientOrTransaction,
|
||||
organizationId: string,
|
||||
config: SourceMetadata["integration"]
|
||||
config: SourceMetadataV1["integration"]
|
||||
) {
|
||||
if (config.authSource === "HOSTED") {
|
||||
return tx.integration.findUnique({
|
||||
@@ -0,0 +1,361 @@
|
||||
import type { Endpoint } from "@trigger.dev/database";
|
||||
import type { SourceMetadataV2 } from "@trigger.dev/core";
|
||||
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import type { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { workerQueue } from "../worker.server";
|
||||
import { generateSecret } from "./utils.server";
|
||||
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
|
||||
|
||||
export class RegisterSourceServiceV2 {
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
|
||||
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(
|
||||
endpointIdOrEndpoint: string | ExtendedEndpoint,
|
||||
metadata: SourceMetadataV2,
|
||||
dynamicTriggerId?: string,
|
||||
accountId?: string,
|
||||
dynamicSource?: { id: string; metadata: any }
|
||||
) {
|
||||
const endpoint =
|
||||
typeof endpointIdOrEndpoint === "string"
|
||||
? await findEndpoint(endpointIdOrEndpoint)
|
||||
: endpointIdOrEndpoint;
|
||||
|
||||
return this.#upsertSource(
|
||||
endpoint,
|
||||
endpoint.environment,
|
||||
metadata,
|
||||
dynamicTriggerId,
|
||||
accountId,
|
||||
dynamicSource
|
||||
);
|
||||
}
|
||||
|
||||
async #upsertSource(
|
||||
endpoint: Endpoint,
|
||||
environment: AuthenticatedEnvironment,
|
||||
metadata: SourceMetadataV2,
|
||||
dynamicTriggerId?: string,
|
||||
accountId?: string,
|
||||
dynamicSource?: { id: string; metadata: any }
|
||||
) {
|
||||
const key = [dynamicTriggerId, dynamicSource?.id, metadata.key].filter(Boolean).join(":");
|
||||
|
||||
const registrationJob = metadata.registerSourceJob
|
||||
? await this.#prismaClient.job.findUnique({
|
||||
where: {
|
||||
projectId_slug: {
|
||||
projectId: endpoint.projectId,
|
||||
slug: metadata.registerSourceJob.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const source = await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
const integration = await this.#findOrCreateIntegration(
|
||||
tx,
|
||||
environment.organizationId,
|
||||
metadata.integration
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
throw new Error("Integration not found");
|
||||
}
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// options
|
||||
const createOptions = Object.entries(metadata.options).flatMap(([name, values]) => {
|
||||
const uniqueValues = [...new Set(values)];
|
||||
return uniqueValues.map((value) => ({ name, value }));
|
||||
});
|
||||
|
||||
const triggerSource = await tx.triggerSource.upsert({
|
||||
where: {
|
||||
key_environmentId: {
|
||||
environmentId: environment.id,
|
||||
key,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
version: "2",
|
||||
params: metadata.params,
|
||||
key,
|
||||
channel: metadata.channel,
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
endpoint: {
|
||||
connect: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
connect: {
|
||||
id: environment.projectId,
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
integration: { connect: { id: integration.id } },
|
||||
dynamicTrigger: dynamicTriggerId
|
||||
? {
|
||||
connect: {
|
||||
id: dynamicTriggerId,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
externalAccount: externalAccount ? { connect: { id: externalAccount.id } } : undefined,
|
||||
options: {
|
||||
create: createOptions,
|
||||
},
|
||||
secretReference: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
key: `${endpoint.id}:${key}`,
|
||||
},
|
||||
create: {
|
||||
key: `${endpoint.id}:${key}`,
|
||||
provider: "DATABASE",
|
||||
},
|
||||
},
|
||||
},
|
||||
dynamicSourceId: dynamicSource?.id,
|
||||
dynamicSourceMetadata: dynamicSource?.metadata,
|
||||
sourceRegistrationJob:
|
||||
registrationJob && metadata.registerSourceJob
|
||||
? {
|
||||
connect: {
|
||||
jobId_version_environmentId: {
|
||||
jobId: registrationJob.id,
|
||||
version: metadata.registerSourceJob.version,
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
update: {
|
||||
version: "2",
|
||||
endpoint: {
|
||||
connect: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
},
|
||||
integration: { connect: { id: integration.id } },
|
||||
dynamicSourceId: dynamicSource?.id,
|
||||
dynamicSourceMetadata: dynamicSource?.metadata,
|
||||
sourceRegistrationJob:
|
||||
registrationJob && metadata.registerSourceJob
|
||||
? {
|
||||
connect: {
|
||||
jobId_version_environmentId: {
|
||||
jobId: registrationJob.id,
|
||||
version: metadata.registerSourceJob.version,
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
options: true,
|
||||
secretReference: true,
|
||||
},
|
||||
});
|
||||
|
||||
switch (metadata.channel) {
|
||||
case "HTTP": {
|
||||
await tx.secretStore.upsert({
|
||||
where: {
|
||||
key: triggerSource.secretReference.key,
|
||||
},
|
||||
create: {
|
||||
key: triggerSource.secretReference.key,
|
||||
value: {
|
||||
secret: generateSecret(),
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the options that are no longer being used so we can remove them
|
||||
const newOptions = metadata.options;
|
||||
const orphanedOptions: Record<string, string[]> = {};
|
||||
for (const event of triggerSource.options) {
|
||||
const values = newOptions[event.name];
|
||||
if (values === undefined) {
|
||||
orphanedOptions[event.name] = [event.value];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (values!.includes(event.value)) {
|
||||
orphanedOptions[event.name] = [...values, event.value];
|
||||
}
|
||||
}
|
||||
|
||||
//add or update the options
|
||||
const flatOptions = Object.entries(newOptions).flatMap(([name, values]) =>
|
||||
values.map((v) => ({ name, value: v }))
|
||||
);
|
||||
for (const { name, value } of flatOptions) {
|
||||
await tx.triggerSourceOption.upsert({
|
||||
where: {
|
||||
name_value_sourceId: {
|
||||
name,
|
||||
value,
|
||||
sourceId: triggerSource.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
name,
|
||||
value,
|
||||
source: {
|
||||
connect: {
|
||||
id: triggerSource.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: triggerSource.id,
|
||||
orphanedOptions,
|
||||
};
|
||||
},
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, orphanedOptions } = source;
|
||||
|
||||
// We need to activate the source if:
|
||||
// 1. It's not active
|
||||
// 2. There are orphaned events
|
||||
// 3. There are trigger events that are not registered
|
||||
const triggerSource = await this.#prismaClient.triggerSource.findUniqueOrThrow({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
include: {
|
||||
options: true,
|
||||
secretReference: true,
|
||||
integration: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dynamicTriggerId) {
|
||||
return triggerSource;
|
||||
}
|
||||
|
||||
const triggerIsActive = triggerSource.active;
|
||||
const triggerHasOrphanedEvents = Object.keys(orphanedOptions).length > 0;
|
||||
const triggerHasUnregisteredEvents = triggerSource.options.some((option) => !option.registered);
|
||||
|
||||
if (!triggerIsActive || triggerHasOrphanedEvents || triggerHasUnregisteredEvents) {
|
||||
// We need to re-activate the source, and there could be orphaned events
|
||||
await workerQueue.enqueue("activateSource", {
|
||||
version: "2",
|
||||
id: triggerSource.id,
|
||||
orphanedOptions,
|
||||
});
|
||||
}
|
||||
|
||||
return triggerSource;
|
||||
}
|
||||
|
||||
async #findOrCreateIntegration(
|
||||
tx: PrismaClientOrTransaction,
|
||||
organizationId: string,
|
||||
config: SourceMetadataV2["integration"]
|
||||
) {
|
||||
if (config.authSource === "HOSTED") {
|
||||
return tx.integration.findUnique({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId,
|
||||
slug: config.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return tx.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId,
|
||||
slug: config.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: config.id,
|
||||
title: config.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: config.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: config.metadata.id,
|
||||
name: config.metadata.name,
|
||||
instructions: config.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: config.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: config.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: config.metadata.id,
|
||||
name: config.metadata.name,
|
||||
instructions: config.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -1,10 +1,10 @@
|
||||
import type { RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import type { TriggerSource, UpdateTriggerSourceBody } from "@trigger.dev/core";
|
||||
import type { TriggerSource, UpdateTriggerSourceBodyV1 } from "@trigger.dev/core";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { getSecretStore } from "../secrets/secretStore.server";
|
||||
|
||||
export class UpdateSourceService {
|
||||
export class UpdateSourceServiceV1 {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
@@ -18,7 +18,7 @@ export class UpdateSourceService {
|
||||
endpointSlug,
|
||||
}: {
|
||||
environment: RuntimeEnvironment;
|
||||
payload: UpdateTriggerSourceBody;
|
||||
payload: UpdateTriggerSourceBodyV1;
|
||||
id: string;
|
||||
endpointSlug: string;
|
||||
}): Promise<TriggerSource> {
|
||||
@@ -55,10 +55,11 @@ export class UpdateSourceService {
|
||||
});
|
||||
|
||||
for (const event of payload.registeredEvents) {
|
||||
await this.#prismaClient.triggerSourceEvent.update({
|
||||
await this.#prismaClient.triggerSourceOption.update({
|
||||
where: {
|
||||
name_sourceId: {
|
||||
name: event,
|
||||
name_value_sourceId: {
|
||||
name: "event",
|
||||
value: event,
|
||||
sourceId: triggerSource.id,
|
||||
},
|
||||
},
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { TriggerSource, UpdateTriggerSourceBodyV2 } from "@trigger.dev/core";
|
||||
import type { RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { getSecretStore } from "../secrets/secretStore.server";
|
||||
|
||||
export class UpdateSourceServiceV2 {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
environment,
|
||||
payload,
|
||||
id,
|
||||
endpointSlug,
|
||||
}: {
|
||||
environment: RuntimeEnvironment;
|
||||
payload: UpdateTriggerSourceBodyV2;
|
||||
id: string;
|
||||
endpointSlug: string;
|
||||
}): Promise<TriggerSource> {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
environmentId: environment.id,
|
||||
slug: endpointSlug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const triggerSource = await this.#prismaClient.triggerSource.findUniqueOrThrow({
|
||||
where: {
|
||||
key_environmentId: {
|
||||
environmentId: environment.id,
|
||||
key: id,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
secretReference: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.#prismaClient.triggerSource.update({
|
||||
where: {
|
||||
id: triggerSource.id,
|
||||
},
|
||||
data: {
|
||||
active: true,
|
||||
channelData: payload.data as any,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
});
|
||||
|
||||
const flatOptions = Object.entries(payload.options).flatMap(([name, value]) =>
|
||||
value.map((v) => ({
|
||||
name,
|
||||
value: v,
|
||||
}))
|
||||
);
|
||||
|
||||
for (const { name, value } of flatOptions) {
|
||||
await this.#prismaClient.triggerSourceOption.update({
|
||||
where: {
|
||||
name_value_sourceId: {
|
||||
name,
|
||||
value,
|
||||
sourceId: triggerSource.id,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
registered: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.secret) {
|
||||
// We need to update the secret reference in the store
|
||||
const secretStore = getSecretStore(triggerSource.secretReference.provider);
|
||||
|
||||
await secretStore.setSecret<{ secret: string }>(triggerSource.secretReference.key, {
|
||||
secret: payload.secret,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: triggerSource.id,
|
||||
key: triggerSource.key,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { InitializeTriggerBody, REGISTER_SOURCE_EVENT } from "@trigger.dev/core";
|
||||
import { InitializeTriggerBody, REGISTER_SOURCE_EVENT_V1 } from "@trigger.dev/core";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterTriggerSourceService } from "./registerTriggerSource.server";
|
||||
import { RegisterTriggerSourceServiceV1 } from "./registerTriggerSourceV1.server";
|
||||
import { IngestSendEvent } from "../events/ingestSendEvent.server";
|
||||
|
||||
export class InitializeTriggerService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerTriggerSource = new RegisterTriggerSourceService();
|
||||
#registerTriggerSource = new RegisterTriggerSourceServiceV1();
|
||||
#sendEvent = new IngestSendEvent();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
@@ -71,7 +71,7 @@ export class InitializeTriggerService {
|
||||
environment,
|
||||
{
|
||||
id: registration.id,
|
||||
name: REGISTER_SOURCE_EVENT,
|
||||
name: REGISTER_SOURCE_EVENT_V1,
|
||||
source: "trigger.dev",
|
||||
payload: {
|
||||
...registration,
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
import { RegisterSourceEvent, RegisterTriggerBody } from "@trigger.dev/core";
|
||||
import { RegisterSourceEventV1, RegisterTriggerBodyV1 } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { getSecretStore } from "../secrets/secretStore.server";
|
||||
import { RegisterSourceService } from "../sources/registerSource.server";
|
||||
import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server";
|
||||
|
||||
export class RegisterTriggerSourceService {
|
||||
export class RegisterTriggerSourceServiceV1 {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
@@ -23,13 +23,13 @@ export class RegisterTriggerSourceService {
|
||||
registrationMetadata,
|
||||
}: {
|
||||
environment: AuthenticatedEnvironment;
|
||||
payload: RegisterTriggerBody;
|
||||
payload: RegisterTriggerBodyV1;
|
||||
id: string;
|
||||
endpointSlug: string;
|
||||
key: string;
|
||||
accountId?: string;
|
||||
registrationMetadata?: any;
|
||||
}): Promise<RegisterSourceEvent | undefined> {
|
||||
}): Promise<RegisterSourceEventV1 | undefined> {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
@@ -52,7 +52,7 @@ export class RegisterTriggerSourceService {
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
const service = new RegisterSourceService(tx);
|
||||
const service = new RegisterSourceServiceV1(tx);
|
||||
|
||||
const triggerSource = await service.call(
|
||||
endpoint.id,
|
||||
@@ -76,7 +76,7 @@ export class RegisterTriggerSourceService {
|
||||
create: {
|
||||
dispatchableId: triggerSource.id,
|
||||
environmentId: environment.id,
|
||||
event: Array.isArray(payload.rule.event) ? payload.rule.event : [payload.rule.event],
|
||||
event: Array.isArray(payload.rule.event) ? payload.rule.event : [payload.rule.event],
|
||||
source: payload.rule.source,
|
||||
payloadFilter: payload.rule.payload,
|
||||
contextFilter: payload.rule.context,
|
||||
@@ -141,7 +141,7 @@ export class RegisterTriggerSourceService {
|
||||
},
|
||||
clientId: triggerSource.integration.slug,
|
||||
},
|
||||
events: triggerSource.events.map((e) => e.name),
|
||||
events: triggerSource.options.map((e) => e.value),
|
||||
missingEvents: [],
|
||||
orphanedEvents: [],
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
RegisterSourceEventOptions,
|
||||
RegisterSourceEventV2,
|
||||
RegisterTriggerBodyV2,
|
||||
RegisteredOptionsDiff,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { getSecretStore } from "../secrets/secretStore.server";
|
||||
import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server";
|
||||
|
||||
export class RegisterTriggerSourceServiceV2 {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
environment,
|
||||
payload,
|
||||
endpointSlug,
|
||||
id,
|
||||
key,
|
||||
accountId,
|
||||
registrationMetadata,
|
||||
}: {
|
||||
environment: AuthenticatedEnvironment;
|
||||
payload: RegisterTriggerBodyV2;
|
||||
id: string;
|
||||
endpointSlug: string;
|
||||
key: string;
|
||||
accountId?: string;
|
||||
registrationMetadata?: any;
|
||||
}): Promise<RegisterSourceEventV2 | undefined> {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
environmentId: environment.id,
|
||||
slug: endpointSlug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dynamicTrigger = await this.#prismaClient.dynamicTrigger.findUniqueOrThrow({
|
||||
where: {
|
||||
endpointId_slug_type: {
|
||||
endpointId: endpoint.id,
|
||||
slug: id,
|
||||
type: "EVENT",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
const service = new RegisterSourceServiceV2(tx);
|
||||
|
||||
const triggerSource = await service.call(
|
||||
endpoint.id,
|
||||
payload.source,
|
||||
dynamicTrigger.id,
|
||||
accountId,
|
||||
{ id: key, metadata: registrationMetadata }
|
||||
);
|
||||
|
||||
if (!triggerSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDispatcher = await tx.eventDispatcher.upsert({
|
||||
where: {
|
||||
dispatchableId_environmentId: {
|
||||
dispatchableId: triggerSource.id,
|
||||
environmentId: environment.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
dispatchableId: triggerSource.id,
|
||||
environmentId: environment.id,
|
||||
event: Array.isArray(payload.rule.event) ? payload.rule.event : [payload.rule.event],
|
||||
source: payload.rule.source,
|
||||
payloadFilter: payload.rule.payload,
|
||||
contextFilter: payload.rule.context,
|
||||
dispatchable: {
|
||||
type: "DYNAMIC_TRIGGER",
|
||||
id: dynamicTrigger.id,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
event: Array.isArray(payload.rule.event) ? payload.rule.event : [payload.rule.event],
|
||||
source: payload.rule.source,
|
||||
payloadFilter: payload.rule.payload,
|
||||
contextFilter: payload.rule.context,
|
||||
dispatchable: {
|
||||
type: "DYNAMIC_TRIGGER",
|
||||
id: dynamicTrigger.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registration = await tx.dynamicTriggerRegistration.upsert({
|
||||
where: {
|
||||
key_dynamicTriggerId: {
|
||||
key,
|
||||
dynamicTriggerId: dynamicTrigger.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
key,
|
||||
dynamicTriggerId: dynamicTrigger.id,
|
||||
sourceId: triggerSource.id,
|
||||
eventDispatcherId: eventDispatcher.id,
|
||||
metadata: registrationMetadata,
|
||||
},
|
||||
update: {
|
||||
metadata: registrationMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
const secretStore = getSecretStore(triggerSource.secretReference.provider, {
|
||||
prismaClient: tx,
|
||||
});
|
||||
|
||||
const { secret } = await secretStore.getSecretOrThrow(
|
||||
z.object({
|
||||
secret: z.string(),
|
||||
}),
|
||||
triggerSource.secretReference.key
|
||||
);
|
||||
|
||||
//turn into required format
|
||||
const optionsArray = Object.entries(payload.source.options).flatMap(([name, values]) => {
|
||||
return { name, values };
|
||||
});
|
||||
const options = optionsArray.reduce((acc, { name, values }) => {
|
||||
acc[name] = {
|
||||
desired: [...new Set(values)],
|
||||
missing: [],
|
||||
orphaned: [],
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, { desired: string[]; missing: string[]; orphaned: string[] }>) as RegisterSourceEventOptions;
|
||||
|
||||
const data: RegisterSourceEventV2 = {
|
||||
id: registration.id,
|
||||
source: {
|
||||
key: triggerSource.key,
|
||||
active: triggerSource.active,
|
||||
params: triggerSource.params,
|
||||
secret,
|
||||
data: triggerSource.channelData as any,
|
||||
channel: {
|
||||
type: "HTTP",
|
||||
url: `${env.APP_ORIGIN}/api/v1/sources/http/${triggerSource.id}`,
|
||||
},
|
||||
clientId: triggerSource.integration.slug,
|
||||
},
|
||||
options,
|
||||
};
|
||||
|
||||
return data;
|
||||
},
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent.
|
||||
import { ActivateSourceService } from "./sources/activateSource.server";
|
||||
import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server";
|
||||
import { PerformTaskOperationService } from "./tasks/performTaskOperation.server";
|
||||
import { addMissingVersionField } from "@trigger.dev/core";
|
||||
|
||||
const workerCatalog = {
|
||||
indexEndpoint: z.object({
|
||||
@@ -37,11 +38,21 @@ const workerCatalog = {
|
||||
organizationId: z.string(),
|
||||
connectionId: z.string(),
|
||||
}),
|
||||
activateSource: z.object({
|
||||
id: z.string(),
|
||||
orphanedEvents: z.array(z.string()).optional(),
|
||||
}),
|
||||
|
||||
activateSource: z.preprocess(
|
||||
addMissingVersionField,
|
||||
z.discriminatedUnion("version", [
|
||||
z.object({
|
||||
version: z.literal("1"),
|
||||
id: z.string(),
|
||||
orphanedEvents: z.array(z.string()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
version: z.literal("2"),
|
||||
id: z.string(),
|
||||
orphanedOptions: z.record(z.string(), z.array(z.string())).optional(),
|
||||
}),
|
||||
])
|
||||
),
|
||||
deliverEvent: z.object({ id: z.string() }),
|
||||
"events.invokeDispatcher": z.object({
|
||||
id: z.string(),
|
||||
@@ -187,10 +198,27 @@ function getWorkerQueue() {
|
||||
activateSource: {
|
||||
priority: 10, // smaller number = higher priority
|
||||
maxAttempts: 3,
|
||||
handler: async (payload, job) => {
|
||||
handler: async (payload, graphileJob) => {
|
||||
const service = new ActivateSourceService();
|
||||
|
||||
await service.call(payload.id, job.id, payload.orphanedEvents);
|
||||
switch (payload.version) {
|
||||
case "1": {
|
||||
//change the input data to match the new schema
|
||||
await service.call(
|
||||
payload.id,
|
||||
graphileJob.id,
|
||||
payload.orphanedEvents
|
||||
? {
|
||||
event: payload.orphanedEvents,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "2": {
|
||||
await service.call(payload.id, graphileJob.id, payload.orphanedOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
deliverHttpSourceRequest: {
|
||||
@@ -292,7 +320,12 @@ function getExecutionWorkerQueue() {
|
||||
handler: async (payload, job) => {
|
||||
const service = new PerformRunExecutionV2Service();
|
||||
|
||||
await service.call(payload.id, payload.reason, payload.isRetry, payload.resumeTaskId);
|
||||
await service.call({
|
||||
id: payload.id,
|
||||
reason: payload.reason,
|
||||
resumeTaskId: payload.resumeTaskId,
|
||||
isRetry: payload.isRetry,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function seedCloud(prisma: PrismaClient) {
|
||||
},
|
||||
create: {
|
||||
apiKey: "tr_prod_bNaLxayOXqoj",
|
||||
pkApiKey: "pk_dev_323f3650218e370508cf",
|
||||
pkApiKey: "pk_dev_323f3650218e378191cf",
|
||||
slug: "prod",
|
||||
type: "PRODUCTION",
|
||||
project: {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const client = new TriggerClient({
|
||||
});
|
||||
```
|
||||
|
||||
View the [Client API Reference](//sdk/triggerclient) for more information.
|
||||
View the [Client API Reference](/sdk/triggerclient) for more information.
|
||||
|
||||
## Adaptors
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ You can view the [source code here](https://github.com/triggerdotdev/examples/tr
|
||||
className="w-full aspect-video"
|
||||
src="https://www.youtube.com/embed/uocBQt2HeQo"
|
||||
title="Create a serverless background job in 10 mins"
|
||||
frameborder="0"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
We cover how to:
|
||||
|
||||
@@ -25,5 +25,3 @@ Click the links below to view the Job code. You can also easily test these Jobs
|
||||
| [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) |
|
||||
| [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) |
|
||||
|
||||
If you have any ideas for Jobs you would like to build, you can fill in the [Job request form](https://bcymafitv0e.typeform.com/to/YLUKy9my#source=example-jobs-docs).
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: The structure of an integration
|
||||
description: "What files are in an Integration package and what do they do?"
|
||||
---
|
||||
|
||||
```sh
|
||||
stripe
|
||||
├── README.md
|
||||
├── package.json
|
||||
├── tsup.config.ts
|
||||
├── src
|
||||
│ ├── index.ts
|
||||
│ ├── tasks.ts
|
||||
│ └── types.ts
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Once you've created your Integration package, you can start developing it. In this stage of development, you'll mainly be making changes to the `src/index.ts`, `src/types.ts`, and `src/tasks.ts` files. Below is an explanation of each of the files and their roles:
|
||||
|
||||
### `src/index.ts`
|
||||
|
||||
This is the entry point of the Integration package and where the client lives. See [the Client docs](/integrations/create-client).
|
||||
|
||||
### `src/types.ts`
|
||||
|
||||
This file contains all of the types that are used in the Integration package. It's a good idea to keep all of the types in this file so that they can be easily imported into both the `src/index.ts` and `src/tasks.ts` files.
|
||||
|
||||
You'll want to export the following types from this file
|
||||
|
||||
- **SDK Client Type** - Export the type of the underlying SDK client that will be used both in authenticated tasks (more below) and the main Integration class.
|
||||
- **Authenticated Task Param Types** - Export a single type for each authenticated task input params
|
||||
- **Authenticated Task Response Types** - Export a single type for each authenticated task output response
|
||||
|
||||
### `src/tasks.ts`
|
||||
|
||||
This file contains all of the authenticated tasks that the Integration will support. Tasks are the main way that developers will interact with your Integration. They are the actions that developers will be able to perform in their jobs.
|
||||
|
||||
For more about how tasks work, see the [tasks documentation](/documentation/concepts/tasks).
|
||||
|
||||
See the guide for creating [authenticated tasks](/integrations/create-tasks) for more information.
|
||||
|
||||
### Triggers
|
||||
|
||||
See the guide for creating [triggers](/integrations/create-triggers) for more information.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Creating the client
|
||||
description: "Each integration has a client that performs the actual HTTP requests. Usually this is a wrapper around an official SDK provided by the service."
|
||||
---
|
||||
|
||||
### `src/index.ts`
|
||||
|
||||
This is where the client should live.
|
||||
|
||||
<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`
|
||||
</Tip>
|
||||
|
||||
<Accordion title="Example: OpenAI">
|
||||
|
||||
```ts
|
||||
import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
|
||||
import { Configuration, OpenAIApi } from "openai";
|
||||
import * as tasks from "./tasks";
|
||||
import { OpenAIIntegrationOptions } from "./types";
|
||||
|
||||
export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, typeof tasks>> {
|
||||
client: IntegrationClient<OpenAIApi, typeof tasks>;
|
||||
|
||||
constructor(private options: OpenAIIntegrationOptions) {
|
||||
this.client = {
|
||||
tasks,
|
||||
usesLocalAuth: true,
|
||||
client: new OpenAIApi(
|
||||
new Configuration({
|
||||
apiKey: options.apiKey,
|
||||
organization: options.organization,
|
||||
})
|
||||
),
|
||||
auth: {
|
||||
apiKey: options.apiKey,
|
||||
organization: options.organization,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "openai", name: "OpenAI" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
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.
|
||||
</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"`.
|
||||
</ParamField>
|
||||
<ParamField body="name" type="string" required>
|
||||
The name of the Integration. For example, the OpenAI Integration has a name of `"OpenAI"`.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="client" type="IntegrationClient" required>
|
||||
An IntegrationClient object that contains either the underlying SDK client (if
|
||||
using local auth) or a `clientFactory` function to create new clients using
|
||||
authenticated credentials.
|
||||
|
||||
{" "}
|
||||
|
||||
<Expandable title="properties" defaultOpen>
|
||||
<ParamField body="usesLocalAuth" type="boolean" required>
|
||||
Specifies whether the client uses local auth or not. If `true`, the `client` property should contain the underlying SDK client. If `false`, the `client` property should contain a `clientFactory` function that can be used to create new clients using authenticated credentials.
|
||||
</ParamField>
|
||||
<ParamField body="tasks" type="Record<string, AuthenticatedTask>" required>
|
||||
An object that contains all of the authenticated tasks that are supported by the Integration. The keys of the object should be the names of the tasks and the values should be the authenticated tasks. More on authenticated tasks below.
|
||||
</ParamField>
|
||||
<ParamField body="clientFactory" type="function">
|
||||
A function that takes in an `auth` object (of type [ConnectionAuth](/sdk/connection-auth)) and returns a new SDK client that is authenticated with the credentials in the `auth` object. This should only be set if `usesLocalAuth` is `false`.
|
||||
</ParamField>
|
||||
<ParamField body="client" type="SDK Client">
|
||||
The underlying SDK client that will be used to make requests to the service. This should only be set if `usesLocalAuth` is `true`.
|
||||
</ParamField>
|
||||
<ParamField body="auth" type="any">
|
||||
The authenticated credentials that were used to create the `client`. This should only be set if `usesLocalAuth` is `true`. This is used to access the credentials inside of authenticated tasks, but is only necessary in specific cases (like when doing a [`backgroundFetch`](/sdk/io/backgroundfetch) as a subtask)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: OAuth
|
||||
description: "OAuth is used to authenticate users by passing them to a third-party service, such as Google or Facebook, to log in."
|
||||
---
|
||||
|
||||
<Note>The guide on adding a new OAuth app to Trigger.dev is coming soon</Note>
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Publishing
|
||||
description: "Publishing an official integration package to the public registry."
|
||||
---
|
||||
|
||||
## 1. Check your integration
|
||||
|
||||
Ensure that you have done the following:
|
||||
|
||||
- [ ] Create the integrations in the `integrations` folder.
|
||||
- [ ] Created a file in the `examples/job-catalog` folder with some example jobs.
|
||||
- [ ] Have exported types.
|
||||
- [ ] Avoid using `any` types and `@ts-ignore` comments. If there are any, please explain them in the PR.
|
||||
|
||||
## 2. Create a Pull Request
|
||||
|
||||
[Create a Pull Request](https://github.com/triggerdotdev/trigger.dev/pulls) in the Trigger.dev repository.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Setup the package
|
||||
description: "How to create the folders and basic files for an integration package."
|
||||
---
|
||||
|
||||
Before you embark on creating an Integration, you have to decide where the Integration code will be located. You have two options, either in the [Trigger.dev monorepo](https://github.com/triggerdotdev/trigger.dev) and namespaced under the `@trigger.dev` NPM organization, or in a separate repository you control and published independently.
|
||||
|
||||
## Using OpenAI to generate the initial code
|
||||
|
||||
When you use the `@trigger.dev/cli create-integration` command you can pass in an OpenAI API key to generate the initial code for your Integration. Use the `-o` option to do this.
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli create-integration -o=sk-abcdefghijk integrations/stripe
|
||||
```
|
||||
|
||||
## In the Trigger.dev monorepo
|
||||
|
||||
Before you can create an Integration in the Trigger.dev monorepo, you'll need to follow our [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) to get your local environment setup.
|
||||
|
||||
Once you've forked the repository and cloned it locally, create a new branch for your Integration and prefix it with the `integrations/` namespace. For example, if you were creating an Integration for [Stripe](https://stripe.com), you would create a branch named `integrations/stripe`.
|
||||
|
||||
```bash
|
||||
git checkout -b integrations/stripe
|
||||
```
|
||||
|
||||
Now you are ready to create your Integration. We've created a CLI tool to help you scaffold out the Integration package. You can run the following command to create a new Integration package in the `integrations` directory:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli create-integration integrations/stripe
|
||||
```
|
||||
|
||||
This will ask you a few questions about your Integration:
|
||||
|
||||
1. `What is the name of your Integration package?` - This is the name of the NPM package that will be created. It should be prefixed with `@trigger.dev/integration-` and be all lowercase. For example, if you were creating an Integration for Stripe, you would enter `@trigger.dev/stripe`.
|
||||
2. `What is the name of the npm package of the Integration?` - This is the name of the NPM package that the Integration will be wrapping. For example, if you were creating an Integration for Stripe, you would enter `stripe`.
|
||||
|
||||
From this point, the CLI will create the Integration package for you and install all of the dependencies. It creates the following package structure:
|
||||
|
||||
```bash
|
||||
integrations
|
||||
└── stripe
|
||||
├── README.md
|
||||
├── package.json
|
||||
├── tsup.config.ts
|
||||
├── src
|
||||
│ ├── index.ts
|
||||
│ ├── tasks.ts
|
||||
│ └── types.ts
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Next, head to the [How to develop an Integration package](#how-to-develop-an-integration-package) section to learn how to develop your Integration.
|
||||
|
||||
## In your own repository
|
||||
|
||||
You can also create an Integration in your own repository and publish it independently. This is useful if you want to keep your Integration code separate from the Trigger.dev monorepo. In this case, you should use the `@trigger.dev/cli` to create the Integration package in your repository or to start a new one:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli create-integration my-internal-integration
|
||||
```
|
||||
@@ -0,0 +1,428 @@
|
||||
---
|
||||
title: Authenticated Tasks
|
||||
description: "Tasks are the main way that developers will interact with your Integration. They are the actions that developers will be able to perform in their jobs."
|
||||
---
|
||||
|
||||
## `src/tasks.ts`
|
||||
|
||||
This file contains all of the authenticated tasks that the Integration will support.
|
||||
|
||||
## Authenticated tasks
|
||||
|
||||
Authenticated tasks are the backbone of an Integration and so we're going to cover them in more detail before moving on the main Integration class.
|
||||
|
||||
Authenticated tasks are a specially crafted object that allows the `@trigger.dev/sdk` to run the task seeded with an authenticated SDK client.
|
||||
|
||||
For example, here is the `getForm` authenticated task defined in the `@trigger.dev/typeform` Integration package:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts tasks.ts
|
||||
import type { AuthenticatedTask } from "@trigger.dev/sdk";
|
||||
import type { GetFormParams, GetFormResponse, TypeformSDK } from "./types";
|
||||
|
||||
export const getForm: AuthenticatedTask<TypeformSDK, GetFormParams, GetFormResponse> = {
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Get Form",
|
||||
params,
|
||||
icon: "typeform",
|
||||
properties: [
|
||||
{
|
||||
label: "Form ID",
|
||||
text: params.uid,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
run: async (params, client) => {
|
||||
return client.forms.get(params);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts types.ts
|
||||
import { Prettify } from "@trigger.dev/integration-kit";
|
||||
import { createClient } from "@typeform/api-client";
|
||||
|
||||
export type TypeformSDK = ReturnType<typeof createClient>;
|
||||
|
||||
export type GetFormParams = {
|
||||
uid: string;
|
||||
};
|
||||
|
||||
export type GetFormResponse = Prettify<Typeform.Form>;
|
||||
```
|
||||
|
||||
```ts usage.ts
|
||||
client.defineJob({
|
||||
id: "typeform-playground",
|
||||
name: "Typeform Playground",
|
||||
version: "0.1.1",
|
||||
integrations: {
|
||||
typeform,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
const form = await io.typeform.getForm("get-form", {
|
||||
uid: payload.formId,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
The first thing to notice is the explicit typing of the `getForm` export as an `AuthenticatedTask<TypeformSDK, GetFormParams, GetFormResponse>`.
|
||||
|
||||
- The first type parameter is the type of the SDK client that will be used to run the task. In this case, it's the `TypeformSDK` type that is exported from the `src/types.ts` file.
|
||||
- The second type parameter is the type of the input params that will be passed to the task. The `params` argument in the `run` and `init` functions will be typed as this type parameter.
|
||||
- The third type parameter is the type of the output response that will be returned from the task. The return type of the `run` function needs to match this type.
|
||||
|
||||
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
|
||||
</Note>
|
||||
|
||||
#### `run` function
|
||||
|
||||
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.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="client" type="type parameter" required>
|
||||
The authenticated SDK client that was seeded into the task.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="task" type="Task">
|
||||
The underlying [task](/documentation/concepts/tasks) object
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="io" type="IO">
|
||||
The [IO](/sdk/io/overview) object that can be used to run subtasks using
|
||||
[`io.runTask`](/sdk/io/runtask)
|
||||
</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
|
||||
</ParamField>
|
||||
|
||||
#### `init` function
|
||||
|
||||
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.
|
||||
</ParamField>
|
||||
|
||||
#### `onError` function
|
||||
|
||||
Authenticated tasks take an optional `onError` function that can be used to handle errors that occur during executing of the `run` function of the task. It takes two arguments:
|
||||
|
||||
<ParamField body="error" type="unknown" required>
|
||||
The error that was thrown during the execution of the `run` function.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="task" type="Task" required>
|
||||
The underlying [task](/documentation/concepts/tasks) object
|
||||
</ParamField>
|
||||
|
||||
The `onError` function allows you to reformated errors that occur during the execution of the `run` function. For example, all the tasks in `@trigger.dev/openai` specify the following `onError` function:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts tasks.ts
|
||||
import { OpenAIErrorSchema } from "./types";
|
||||
|
||||
function onTaskError(error: unknown) {
|
||||
const openAIError = OpenAIErrorSchema.safeParse(error);
|
||||
|
||||
if (!openAIError.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, code, type } = openAIError.data.response.data.error;
|
||||
|
||||
return new Error(`${type}: ${message}${code ? ` (${code})` : ""}`);
|
||||
}
|
||||
```
|
||||
|
||||
```ts types.ts
|
||||
const OpenAIErrorSchema = z.object({
|
||||
response: z.object({
|
||||
data: z.object({
|
||||
error: z.object({
|
||||
code: z.string().nullable().optional(),
|
||||
message: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
You can also use the `onError` function to specify a specific time the task should be retried. The `@trigger.dev/github` Integration uses this to retry rate-limited requests:
|
||||
|
||||
```ts
|
||||
function isRequestError(error: unknown): error is RequestError {
|
||||
return typeof error === "object" && error !== null && "status" in error;
|
||||
}
|
||||
|
||||
function onError(error: unknown) {
|
||||
if (!isRequestError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a rate limit error
|
||||
if (error.status === 403 && error.response) {
|
||||
const rateLimitRemaining = error.response.headers["x-ratelimit-remaining"];
|
||||
const rateLimitReset = error.response.headers["x-ratelimit-reset"];
|
||||
|
||||
if (rateLimitRemaining === "0" && rateLimitReset) {
|
||||
const resetDate = new Date(Number(rateLimitReset) * 1000);
|
||||
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
<Note>The guide on creating Integration triggers is coming soon</Note>
|
||||
|
||||
## Testing an Integration package
|
||||
|
||||
<Note>This section is coming soon</Note>
|
||||
|
||||
## Publishing an Integration package
|
||||
|
||||
<Note>This section is coming soon</Note>
|
||||
|
||||
## Example authenticated tasks
|
||||
|
||||
### OpenAI examples
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts retrieveModel
|
||||
export const retrieveModel: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<RetrieveModelRequest>,
|
||||
RetrieveModelResponseData
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.retrieveModel(params.model).then((res) => res.data);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Retrieve model",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Model id",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts createCompletion
|
||||
export const createCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateCompletionRequest>,
|
||||
Prettify<Awaited<ReturnType<OpenAIClientType["createCompletion"]>>["data"]>
|
||||
> = {
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.createCompletion(params);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.data.usage);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts backgroundCompletion
|
||||
export const backgroundCreateCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateCompletionRequest>,
|
||||
Prettify<CreateCompletionResponseData>,
|
||||
OpenAIIntegrationAuth
|
||||
> = {
|
||||
run: async (params, client, task, io, auth) => {
|
||||
const response = await io.backgroundFetch<CreateCompletionResponseData>(
|
||||
"background",
|
||||
"https://api.openai.com/v1/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: redactString`Bearer ${auth.apiKey}`,
|
||||
...(auth.organization ? { "OpenAI-Organization": auth.organization } : {}),
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
}
|
||||
);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Background Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### GitHub examples
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts createIssue
|
||||
const createIssue: GithubAuthenticatedTask<
|
||||
{ title: string; owner: string; repo: string },
|
||||
OctokitClient["rest"]["issues"]["create"]
|
||||
> = {
|
||||
onError,
|
||||
run: async (params, client, task, io) => {
|
||||
return client.rest.issues
|
||||
.create({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
title: params.title,
|
||||
})
|
||||
.then((res) => res.data);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create Issue",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Title",
|
||||
text: params.title,
|
||||
},
|
||||
],
|
||||
retry: {
|
||||
limit: 3,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 500,
|
||||
maxTimeoutInMs: 30000,
|
||||
randomize: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts createIssueCommentWithReaction
|
||||
const createIssueCommentWithReaction: GithubAuthenticatedTask<
|
||||
{
|
||||
body: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
issueNumber: number;
|
||||
reaction: ReactionContent;
|
||||
},
|
||||
OctokitClient["rest"]["issues"]["createComment"]
|
||||
> = {
|
||||
onError,
|
||||
run: async (params, client, task, io, auth) => {
|
||||
const comment = await io.runTask(
|
||||
`Comment on Issue #${params.issueNumber}`,
|
||||
createIssueComment.init(params),
|
||||
async (t) => {
|
||||
return createIssueComment.run(params, client, t, io, auth);
|
||||
}
|
||||
);
|
||||
|
||||
await io.runTask(
|
||||
`React with ${params.reaction}`,
|
||||
addIssueCommentReaction.init({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
commentId: comment.id,
|
||||
content: params.reaction,
|
||||
}),
|
||||
async (t) => {
|
||||
return addIssueCommentReaction.run(
|
||||
{
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
commentId: comment.id,
|
||||
content: params.reaction,
|
||||
},
|
||||
client,
|
||||
t,
|
||||
io,
|
||||
auth
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return comment;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create Issue Comment",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
{
|
||||
label: "Issue",
|
||||
text: `#${params.issueNumber}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Triggers
|
||||
description: "Triggers cause a Job Run to start. Webhooks and polling are the most relevant for integrations."
|
||||
---
|
||||
|
||||
<Note>The guide on creating Integration triggers is coming soon</Note>
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Create an Integration
|
||||
title: Introduction
|
||||
description: "You can create Integrations of your own."
|
||||
---
|
||||
|
||||
@@ -7,592 +7,10 @@ Before creating an Integration, make sure to read the [Integration overview](/do
|
||||
|
||||
What we'll cover in this guide:
|
||||
|
||||
- [Creating an Integration package](#creating-an-integration-package)
|
||||
- [Developing an Integration package](#developing-an-integration-package)
|
||||
- [Testing an Integration package](#testing-an-integration-package)
|
||||
- [Publishing an Integration package](#publishing-an-integration-package)
|
||||
|
||||
## Creating an Integration package
|
||||
|
||||
Before you embark on creating an Integration, you have to decide where the Integration code will be located. You have two options, either in the [Trigger.dev monorepo](https://github.com/triggerdotdev/trigger.dev) and namespaced under the `@trigger.dev` NPM organization, or in a separate repository you control and published independently.
|
||||
|
||||
### In the Trigger.dev monorepo
|
||||
|
||||
Before you can create an Integration in the Trigger.dev monorepo, you'll need to follow our [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) to get your local environment setup.
|
||||
|
||||
Once you've forked the repository and cloned it locally, create a new branch for your Integration and prefix it with the `integrations/` namespace. For example, if you were creating an Integration for [Stripe](https://stripe.com), you would create a branch named `integrations/stripe`.
|
||||
|
||||
```bash
|
||||
git checkout -b integrations/stripe
|
||||
```
|
||||
|
||||
Now you are ready to create your Integration. We've created a CLI tool to help you scaffold out the Integration package. You can run the following command to create a new Integration package in the `integrations` directory:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli create-integration integrations/stripe
|
||||
```
|
||||
|
||||
This will ask you a few questions about your Integration:
|
||||
|
||||
1. `What is the name of your Integration package?` - This is the name of the NPM package that will be created. It should be prefixed with `@trigger.dev/integration-` and be all lowercase. For example, if you were creating an Integration for Stripe, you would enter `@trigger.dev/stripe`.
|
||||
2. `What is the name of the npm package of the Integration?` - This is the name of the NPM package that the Integration will be wrapping. For example, if you were creating an Integration for Stripe, you would enter `stripe`.
|
||||
|
||||
From this point, the CLI will create the Integration package for you and install all of the dependencies. It creates the following package structure:
|
||||
|
||||
```bash
|
||||
integrations
|
||||
└── stripe
|
||||
├── README.md
|
||||
├── package.json
|
||||
├── tsup.config.ts
|
||||
├── src
|
||||
│ ├── index.ts
|
||||
│ ├── tasks.ts
|
||||
│ └── types.ts
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Next, head to the [How to develop an Integration package](#how-to-develop-an-integration-package) section to learn how to develop your Integration.
|
||||
|
||||
### In your own repository
|
||||
|
||||
You can also create an Integration in your own repository and publish it independently. This is useful if you want to keep your Integration code separate from the Trigger.dev monorepo. In this case, you should use the `@trigger.dev/cli` to create the Integration package in your repository or to start a new one:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli create-integration my-internal-integration
|
||||
```
|
||||
|
||||
## Developing an Integration package
|
||||
|
||||
Once you've created your Integration package, you can start developing it. In this stage of development, you'll mainly be making changes to the `src/index.ts`, `src/types.ts`, and `src/tasks.ts` files. Below is an explanation of each of the files and their roles:
|
||||
|
||||
### `src/index.ts`
|
||||
|
||||
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`
|
||||
</Tip>
|
||||
|
||||
<Accordion title="Example: OpenAI">
|
||||
|
||||
```ts
|
||||
import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
|
||||
import { Configuration, OpenAIApi } from "openai";
|
||||
import * as tasks from "./tasks";
|
||||
import { OpenAIIntegrationOptions } from "./types";
|
||||
|
||||
export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, typeof tasks>> {
|
||||
client: IntegrationClient<OpenAIApi, typeof tasks>;
|
||||
|
||||
constructor(private options: OpenAIIntegrationOptions) {
|
||||
this.client = {
|
||||
tasks,
|
||||
usesLocalAuth: true,
|
||||
client: new OpenAIApi(
|
||||
new Configuration({
|
||||
apiKey: options.apiKey,
|
||||
organization: options.organization,
|
||||
})
|
||||
),
|
||||
auth: {
|
||||
apiKey: options.apiKey,
|
||||
organization: options.organization,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "openai", name: "OpenAI" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
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.
|
||||
</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"`.
|
||||
</ParamField>
|
||||
<ParamField body="name" type="string" required>
|
||||
The name of the Integration. For example, the OpenAI Integration has a name of `"OpenAI"`.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="client" type="IntegrationClient" required>
|
||||
An IntegrationClient object that contains either the underlying SDK client (if
|
||||
using local auth) or a `clientFactory` function to create new clients using
|
||||
authenticated credentials.
|
||||
|
||||
{" "}
|
||||
|
||||
<Expandable title="properties" defaultOpen>
|
||||
<ParamField body="usesLocalAuth" type="boolean" required>
|
||||
Specifies whether the client uses local auth or not. If `true`, the `client` property should contain the underlying SDK client. If `false`, the `client` property should contain a `clientFactory` function that can be used to create new clients using authenticated credentials.
|
||||
</ParamField>
|
||||
<ParamField body="tasks" type="Record<string, AuthenticatedTask>" required>
|
||||
An object that contains all of the authenticated tasks that are supported by the Integration. The keys of the object should be the names of the tasks and the values should be the authenticated tasks. More on authenticated tasks below.
|
||||
</ParamField>
|
||||
<ParamField body="clientFactory" type="function">
|
||||
A function that takes in an `auth` object (of type [ConnectionAuth](/sdk/connection-auth)) and returns a new SDK client that is authenticated with the credentials in the `auth` object. This should only be set if `usesLocalAuth` is `false`.
|
||||
</ParamField>
|
||||
<ParamField body="client" type="SDK Client">
|
||||
The underlying SDK client that will be used to make requests to the service. This should only be set if `usesLocalAuth` is `true`.
|
||||
</ParamField>
|
||||
<ParamField body="auth" type="any">
|
||||
The authenticated credentials that were used to create the `client`. This should only be set if `usesLocalAuth` is `true`. This is used to access the credentials inside of authenticated tasks, but is only necessary in specific cases (like when doing a [`backgroundFetch`](/sdk/io/backgroundfetch) as a subtask)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### `src/types.ts`
|
||||
|
||||
This file contains all of the types that are used in the Integration package. It's a good idea to keep all of the types in this file so that they can be easily imported into both the `src/index.ts` and `src/tasks.ts` files.
|
||||
|
||||
You'll want to export the following types from this file
|
||||
|
||||
- **SDK Client Type** - Export the type of the underlying SDK client that will be used both in authenticated tasks (more below) and the main Integration class.
|
||||
- **Authenticated Task Param Types** - Export a single type for each authenticated task input params
|
||||
- **Authenticated Task Response Types** - Export a single type for each authenticated task output response
|
||||
|
||||
### `src/tasks.ts`
|
||||
|
||||
This file contains all of the authenticated tasks that the Integration will support. Tasks are the main way that developers will interact with your Integration. They are the actions that developers will be able to perform in their jobs.
|
||||
|
||||
For more about how tasks workd, see the [tasks documentation](/documentation/concepts/tasks).
|
||||
|
||||
### Authenticated tasks
|
||||
|
||||
Authenticated tasks are the backbone of an Integration and so we're going to cover them in more detail before moving on the main Integration class.
|
||||
|
||||
Authenticated tasks are a specially crafted object that allows the `@trigger.dev/sdk` to run the task seeded with an authenticated SDK client.
|
||||
|
||||
For example, here is the `getForm` authenticated task defined in the `@trigger.dev/typeform` Integration package:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts tasks.ts
|
||||
import type { AuthenticatedTask } from "@trigger.dev/sdk";
|
||||
import type { GetFormParams, GetFormResponse, TypeformSDK } from "./types";
|
||||
|
||||
export const getForm: AuthenticatedTask<TypeformSDK, GetFormParams, GetFormResponse> = {
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Get Form",
|
||||
params,
|
||||
icon: "typeform",
|
||||
properties: [
|
||||
{
|
||||
label: "Form ID",
|
||||
text: params.uid,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
run: async (params, client) => {
|
||||
return client.forms.get(params);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts types.ts
|
||||
import { Prettify } from "@trigger.dev/integration-kit";
|
||||
import { createClient } from "@typeform/api-client";
|
||||
|
||||
export type TypeformSDK = ReturnType<typeof createClient>;
|
||||
|
||||
export type GetFormParams = {
|
||||
uid: string;
|
||||
};
|
||||
|
||||
export type GetFormResponse = Prettify<Typeform.Form>;
|
||||
```
|
||||
|
||||
```ts usage.ts
|
||||
client.defineJob({
|
||||
id: "typeform-playground",
|
||||
name: "Typeform Playground",
|
||||
version: "0.1.1",
|
||||
integrations: {
|
||||
typeform,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
const form = await io.typeform.getForm("get-form", {
|
||||
uid: payload.formId,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
The first thing to notice is the explicit typing of the `getForm` export as an `AuthenticatedTask<TypeformSDK, GetFormParams, GetFormResponse>`.
|
||||
|
||||
- The first type parameter is the type of the SDK client that will be used to run the task. In this case, it's the `TypeformSDK` type that is exported from the `src/types.ts` file.
|
||||
- The second type parameter is the type of the input params that will be passed to the task. The `params` argument in the `run` and `init` functions will be typed as this type parameter.
|
||||
- The third type parameter is the type of the output response that will be returned from the task. The return type of the `run` function needs to match this type.
|
||||
|
||||
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
|
||||
</Note>
|
||||
|
||||
#### `run` function
|
||||
|
||||
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.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="client" type="type parameter" required>
|
||||
The authenticated SDK client that was seeded into the task.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="task" type="Task">
|
||||
The underlying [task](/documentation/concepts/tasks) object
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="io" type="IO">
|
||||
The [IO](/sdk/io/overview) object that can be used to run subtasks using
|
||||
[`io.runTask`](/sdk/io/runtask)
|
||||
</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
|
||||
</ParamField>
|
||||
|
||||
#### `init` function
|
||||
|
||||
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.
|
||||
</ParamField>
|
||||
|
||||
#### `onError` function
|
||||
|
||||
Authenticated tasks take an optional `onError` function that can be used to handle errors that occur during executing of the `run` function of the task. It takes two arguments:
|
||||
|
||||
<ParamField body="error" type="unknown" required>
|
||||
The error that was thrown during the execution of the `run` function.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="task" type="Task" required>
|
||||
The underlying [task](/documentation/concepts/tasks) object
|
||||
</ParamField>
|
||||
|
||||
The `onError` function allows you to reformated errors that occur during the execution of the `run` function. For example, all the tasks in `@trigger.dev/openai` specify the following `onError` function:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts tasks.ts
|
||||
import { OpenAIErrorSchema } from "./types";
|
||||
|
||||
function onTaskError(error: unknown) {
|
||||
const openAIError = OpenAIErrorSchema.safeParse(error);
|
||||
|
||||
if (!openAIError.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, code, type } = openAIError.data.response.data.error;
|
||||
|
||||
return new Error(`${type}: ${message}${code ? ` (${code})` : ""}`);
|
||||
}
|
||||
```
|
||||
|
||||
```ts types.ts
|
||||
const OpenAIErrorSchema = z.object({
|
||||
response: z.object({
|
||||
data: z.object({
|
||||
error: z.object({
|
||||
code: z.string().nullable().optional(),
|
||||
message: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
You can also use the `onError` function to specify a specific time the task should be retried. The `@trigger.dev/github` Integration uses this to retry rate-limited requests:
|
||||
|
||||
```ts
|
||||
function isRequestError(error: unknown): error is RequestError {
|
||||
return typeof error === "object" && error !== null && "status" in error;
|
||||
}
|
||||
|
||||
function onError(error: unknown) {
|
||||
if (!isRequestError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a rate limit error
|
||||
if (error.status === 403 && error.response) {
|
||||
const rateLimitRemaining = error.response.headers["x-ratelimit-remaining"];
|
||||
const rateLimitReset = error.response.headers["x-ratelimit-reset"];
|
||||
|
||||
if (rateLimitRemaining === "0" && rateLimitReset) {
|
||||
const resetDate = new Date(Number(rateLimitReset) * 1000);
|
||||
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
<Note>The guide on creating Integration triggers is coming soon</Note>
|
||||
|
||||
## Testing an Integration package
|
||||
|
||||
<Note>This section is coming soon</Note>
|
||||
|
||||
## Publishing an Integration package
|
||||
|
||||
<Note>This section is coming soon</Note>
|
||||
|
||||
## Example authenticated tasks
|
||||
|
||||
### OpenAI examples
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts retrieveModel
|
||||
export const retrieveModel: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<RetrieveModelRequest>,
|
||||
RetrieveModelResponseData
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.retrieveModel(params.model).then((res) => res.data);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Retrieve model",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Model id",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts createCompletion
|
||||
export const createCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateCompletionRequest>,
|
||||
Prettify<Awaited<ReturnType<OpenAIClientType["createCompletion"]>>["data"]>
|
||||
> = {
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.createCompletion(params);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.data.usage);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts backgroundCompletion
|
||||
export const backgroundCreateCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateCompletionRequest>,
|
||||
Prettify<CreateCompletionResponseData>,
|
||||
OpenAIIntegrationAuth
|
||||
> = {
|
||||
run: async (params, client, task, io, auth) => {
|
||||
const response = await io.backgroundFetch<CreateCompletionResponseData>(
|
||||
"background",
|
||||
"https://api.openai.com/v1/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: redactString`Bearer ${auth.apiKey}`,
|
||||
...(auth.organization ? { "OpenAI-Organization": auth.organization } : {}),
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
}
|
||||
);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Background Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### GitHub examples
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts createIssue
|
||||
const createIssue: GithubAuthenticatedTask<
|
||||
{ title: string; owner: string; repo: string },
|
||||
OctokitClient["rest"]["issues"]["create"]
|
||||
> = {
|
||||
onError,
|
||||
run: async (params, client, task, io) => {
|
||||
return client.rest.issues
|
||||
.create({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
title: params.title,
|
||||
})
|
||||
.then((res) => res.data);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create Issue",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Title",
|
||||
text: params.title,
|
||||
},
|
||||
],
|
||||
retry: {
|
||||
limit: 3,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 500,
|
||||
maxTimeoutInMs: 30000,
|
||||
randomize: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts createIssueCommentWithReaction
|
||||
const createIssueCommentWithReaction: GithubAuthenticatedTask<
|
||||
{
|
||||
body: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
issueNumber: number;
|
||||
reaction: ReactionContent;
|
||||
},
|
||||
OctokitClient["rest"]["issues"]["createComment"]
|
||||
> = {
|
||||
onError,
|
||||
run: async (params, client, task, io, auth) => {
|
||||
const comment = await io.runTask(
|
||||
`Comment on Issue #${params.issueNumber}`,
|
||||
createIssueComment.init(params),
|
||||
async (t) => {
|
||||
return createIssueComment.run(params, client, t, io, auth);
|
||||
}
|
||||
);
|
||||
|
||||
await io.runTask(
|
||||
`React with ${params.reaction}`,
|
||||
addIssueCommentReaction.init({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
commentId: comment.id,
|
||||
content: params.reaction,
|
||||
}),
|
||||
async (t) => {
|
||||
return addIssueCommentReaction.run(
|
||||
{
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
commentId: comment.id,
|
||||
content: params.reaction,
|
||||
},
|
||||
client,
|
||||
t,
|
||||
io,
|
||||
auth
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return comment;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create Issue Comment",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
{
|
||||
label: "Issue",
|
||||
text: `#${params.issueNumber}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
- [Setup the package](/integrations/create-setup)
|
||||
- [The structure of an integration](/integrations/create-anatomy)
|
||||
- [OAuth](/integrations/create-oauth)
|
||||
- [The integration client](/integrations/create-client)
|
||||
- [Creating Tasks](/integrations/create-tasks)
|
||||
- [Creating Triggers](/integrations/create-triggers)
|
||||
- [Publishing an Integration package](/integrations/create-publishing)
|
||||
|
||||
+13
-1
@@ -173,7 +173,19 @@
|
||||
"group": "Overview",
|
||||
"pages": [
|
||||
"integrations/introduction",
|
||||
"integrations/create"
|
||||
{
|
||||
"group": "Create an Integration",
|
||||
"pages": [
|
||||
"integrations/create",
|
||||
"integrations/create-setup",
|
||||
"integrations/create-anatomy",
|
||||
"integrations/create-oauth",
|
||||
"integrations/create-client",
|
||||
"integrations/create-tasks",
|
||||
"integrations/create-triggers",
|
||||
"integrations/create-publishing"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
"schedules": "nodemon --watch src/schedules.ts -r tsconfig-paths/register -r dotenv/config src/schedules.ts",
|
||||
"stressTest": "nodemon --watch src/stressTest.ts -r tsconfig-paths/register -r dotenv/config src/stressTest.ts",
|
||||
"delays": "nodemon --watch src/delays.ts -r tsconfig-paths/register -r dotenv/config src/delays.ts",
|
||||
"airtable": "nodemon --watch src/airtable.ts -r tsconfig-paths/register -r dotenv/config src/airtable.ts",
|
||||
"resend": "nodemon --watch src/resend.ts -r tsconfig-paths/register -r dotenv/config src/resend.ts",
|
||||
"github": "nodemon --watch src/github.ts -r tsconfig-paths/register -r dotenv/config src/github.ts",
|
||||
"plain": "nodemon --watch src/plain.ts -r tsconfig-paths/register -r dotenv/config src/plain.ts",
|
||||
"typeform": "nodemon --watch src/typeform.ts -r tsconfig-paths/register -r dotenv/config src/typeform.ts",
|
||||
"dynamic-schedule": "nodemon --watch src/dynamic-schedule.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-schedule.ts",
|
||||
"dynamic-triggers": "nodemon --watch src/dynamic-triggers.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-triggers.ts",
|
||||
"background-fetch": "nodemon --watch src/background-fetch.ts -r tsconfig-paths/register -r dotenv/config src/background-fetch.ts",
|
||||
"dev:trigger": "trigger-cli dev --port 8080"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -29,7 +37,8 @@
|
||||
"@trigger.dev/supabase": "workspace:*",
|
||||
"@types/node": "20.4.2",
|
||||
"typescript": "5.1.6",
|
||||
"zod": "3.21.4"
|
||||
"zod": "3.21.4",
|
||||
"@trigger.dev/airtable": "workspace:*"
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "job-catalog"
|
||||
@@ -43,4 +52,4 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^3.14.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { z } from "zod";
|
||||
import { Airtable, Collaborator } from "@trigger.dev/airtable";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
// const airtable = new Airtable({
|
||||
// id: "airtable",
|
||||
// token: process.env["AIRTABLE_TOKEN"],
|
||||
// });
|
||||
|
||||
const airtable = new Airtable({
|
||||
id: "airtable-oauth",
|
||||
});
|
||||
|
||||
type Status = "Live" | "Complete" | "In progress" | "Planning" | "In reviews";
|
||||
|
||||
type LaunchGoalsAndOkRs = {
|
||||
"Launch goals"?: string;
|
||||
DRI?: Collaborator;
|
||||
Team?: string;
|
||||
Status?: "On track" | "In progress" | "At risk";
|
||||
"Key results"?: Array<string>;
|
||||
"Features (from 💻 Features table)"?: Array<string>;
|
||||
"Status (from 💻 Features)": Array<Status>;
|
||||
};
|
||||
|
||||
client.defineJob({
|
||||
id: "airtable-example-1",
|
||||
name: "Airtable Example 1: getRecords",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({
|
||||
name: "airtable.example",
|
||||
schema: z.object({
|
||||
baseId: z.string(),
|
||||
tableName: z.string(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
airtable,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
const table = io.airtable.base(payload.baseId).table<LaunchGoalsAndOkRs>(payload.tableName);
|
||||
|
||||
const records = await table.getRecords("muliple records", { fields: ["Status"] });
|
||||
await io.logger.log(records[0].fields.Status ?? "no status");
|
||||
|
||||
const aRecord = await table.getRecord("single", records[0].id);
|
||||
|
||||
const newRecords = await table.createRecords("create records", [
|
||||
{
|
||||
fields: { "Launch goals": "Created from Trigger.dev", Status: "In progress" },
|
||||
},
|
||||
]);
|
||||
|
||||
const updatedRecords = await table.updateRecords(
|
||||
"update records",
|
||||
newRecords.map((record) => ({
|
||||
id: record.id,
|
||||
fields: { Status: "At risk" },
|
||||
}))
|
||||
);
|
||||
|
||||
await io.wait("5 secs", 5);
|
||||
|
||||
const deletedRecords = await table.deleteRecords(
|
||||
"delete records",
|
||||
updatedRecords.map((record) => record.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
//todo webhooks require batch support
|
||||
// client.defineJob({
|
||||
// id: "airtable-delete-webhooks",
|
||||
// name: "Airtable Example 2: webhook admin",
|
||||
// version: "0.1.0",
|
||||
// trigger: eventTrigger({
|
||||
// name: "airtable.example",
|
||||
// schema: z.object({
|
||||
// baseId: z.string(),
|
||||
// deleteWebhooks: z.boolean().optional(),
|
||||
// }),
|
||||
// }),
|
||||
// integrations: {
|
||||
// airtable,
|
||||
// },
|
||||
// run: async (payload, io, ctx) => {
|
||||
// const webhooks = await io.airtable.webhooks().list("list webhooks", { baseId: payload.baseId });
|
||||
|
||||
// if (payload.deleteWebhooks === true) {
|
||||
// for (const webhook of webhooks.webhooks) {
|
||||
// await io.airtable.webhooks().delete(`delete webhook: ${webhook.id}`, {
|
||||
// baseId: payload.baseId,
|
||||
// webhookId: webhook.id,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
//todo changes the structure of the trigger so it's airtable.base("val").onChanges({
|
||||
// client.defineJob({
|
||||
// id: "airtable-on-table",
|
||||
// name: "Airtable Example: onTable",
|
||||
// version: "0.1.0",
|
||||
// trigger: airtable.onTableChanges({
|
||||
// baseId: "appSX6ly4nZGfdUSy",
|
||||
// tableId: "tblr5BReu2yeOMk7n",
|
||||
// }),
|
||||
// run: async (payload, io, ctx) => {
|
||||
// await io.logger.log(`transaction number ${payload.baseTransactionNumber}`);
|
||||
// },
|
||||
// });
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "test-background-fetch-retry",
|
||||
name: "Test background fetch retry",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "test.background-fetch",
|
||||
schema: z.object({
|
||||
url: z.string(),
|
||||
method: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
body: z.any().optional(),
|
||||
retry: z.any().optional(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
return await io.backgroundFetch<any>(
|
||||
"fetch",
|
||||
payload.url,
|
||||
{
|
||||
method: payload.method ?? "GET",
|
||||
headers: payload.headers,
|
||||
body: payload.body ? JSON.stringify(payload.body) : undefined,
|
||||
},
|
||||
payload.retry
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { DynamicSchedule, TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
const dynamicSchedule = new DynamicSchedule(client, {
|
||||
id: "dynamic-interval",
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "get-user-repo-on-schedule",
|
||||
name: "Get User Repo On Schedule",
|
||||
version: "0.1.1",
|
||||
trigger: dynamicSchedule,
|
||||
|
||||
run: async (payload, io, ctx) => {
|
||||
io.logger.log("Hello World");
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "register-dynamic-interval",
|
||||
name: "Register Dynamic Interval",
|
||||
version: "0.1.1",
|
||||
trigger: eventTrigger({
|
||||
name: "dynamic.interval",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
seconds: z.number().int().positive(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.registerInterval("📆", dynamicSchedule, payload.id, {
|
||||
seconds: payload.seconds,
|
||||
});
|
||||
|
||||
await io.wait("wait", payload.seconds + 10);
|
||||
|
||||
await io.unregisterInterval("❌📆", dynamicSchedule, payload.id);
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "register-dynamic-cron",
|
||||
name: "Register Dynamic Cron",
|
||||
version: "0.1.1",
|
||||
trigger: eventTrigger({
|
||||
name: "dynamic.cron",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
cron: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.registerCron("📆", dynamicSchedule, payload.id, {
|
||||
cron: payload.cron,
|
||||
});
|
||||
|
||||
await io.wait("wait", 60);
|
||||
|
||||
await io.unregisterCron("❌📆", dynamicSchedule, payload.id);
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { Github, events } from "@trigger.dev/github";
|
||||
import { DynamicTrigger, TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
const github = new Github({
|
||||
id: "github-api-key",
|
||||
token: process.env["GITHUB_API_KEY"]!,
|
||||
});
|
||||
|
||||
const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, {
|
||||
id: "github-issue-opened",
|
||||
event: events.onIssueOpened,
|
||||
source: github.sources.repo,
|
||||
});
|
||||
|
||||
const dynamicUserTrigger = new DynamicTrigger(client, {
|
||||
id: "dynamic-user-trigger",
|
||||
event: events.onIssueOpened,
|
||||
source: github.sources.repo,
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "register-dynamic-trigger-on-new-repo",
|
||||
name: "Register dynamic trigger on new repo",
|
||||
version: "0.1.1",
|
||||
trigger: eventTrigger({
|
||||
name: "new.repo",
|
||||
schema: z.object({ owner: z.string(), repo: z.string() }),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
return await io.registerTrigger("register-repo", dynamicOnIssueOpenedTrigger, payload.repo, {
|
||||
owner: payload.owner,
|
||||
repo: payload.repo,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "listen-for-dynamic-trigger",
|
||||
name: "Listen for dynamic trigger",
|
||||
version: "0.1.1",
|
||||
trigger: dynamicOnIssueOpenedTrigger,
|
||||
|
||||
run: async (payload, io, ctx) => {},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "listen-for-dynamic-trigger-2",
|
||||
name: "Listen for dynamic trigger-2",
|
||||
version: "0.1.1",
|
||||
trigger: dynamicOnIssueOpenedTrigger,
|
||||
|
||||
run: async (payload, io, ctx) => {},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "user-on-issue-opened",
|
||||
name: "user on issue opened",
|
||||
version: "0.1.1",
|
||||
trigger: dynamicUserTrigger,
|
||||
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("user-on-issue-opened", { ctx });
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -19,7 +19,7 @@ client.defineJob({
|
||||
name: "event.example",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.runTask("task-example-1", { name: "Task 1" }, async () => {
|
||||
await io.runTask("task-example-1", async () => {
|
||||
return {
|
||||
message: "Hello World",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { Github, events } from "@trigger.dev/github";
|
||||
import { Slack } from "@trigger.dev/slack";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
const githubApiKey = new Github({
|
||||
id: "github-api-key",
|
||||
token: process.env["GITHUB_API_KEY"]!,
|
||||
});
|
||||
|
||||
const github = new Github({
|
||||
id: "github",
|
||||
octokitRequest: { fetch },
|
||||
});
|
||||
|
||||
const slack = new Slack({ id: "my-slack-new" });
|
||||
|
||||
client.defineJob({
|
||||
id: "github-create-issue",
|
||||
name: "GitHub Integration - Create issue",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({
|
||||
name: "create-issue",
|
||||
schema: z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
github: githubApiKey,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
const issue = await io.github.createIssue("create issue", {
|
||||
title: payload.title,
|
||||
owner: payload.owner,
|
||||
repo: payload.repo,
|
||||
});
|
||||
|
||||
await io.github.createIssueCommentWithReaction("comment on issue with reaction", {
|
||||
body: "This is a comment",
|
||||
owner: payload.owner,
|
||||
repo: payload.repo,
|
||||
issueNumber: issue.number,
|
||||
reaction: "heart",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-issue",
|
||||
name: "GitHub Integration - On Issue",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onIssue,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-issue-opened",
|
||||
name: "GitHub Integration - On Issue Opened",
|
||||
version: "0.1.0",
|
||||
integrations: { github: githubApiKey },
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onIssueOpened,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.github.addIssueAssignees("add assignee", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
issueNumber: payload.issue.number,
|
||||
assignees: ["matt-aitken"],
|
||||
});
|
||||
|
||||
await io.github.addIssueLabels("add label", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
issueNumber: payload.issue.number,
|
||||
labels: ["bug"],
|
||||
});
|
||||
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-issue-assigned",
|
||||
name: "GitHub Integration - On Issue assigned",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onIssueAssigned,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-issue-commented",
|
||||
name: "GitHub Integration - On Issue commented",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onIssueComment,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "star-slack-notification",
|
||||
name: "New Star Slack Notification",
|
||||
version: "0.1.0",
|
||||
integrations: { slack },
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewStar,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
const response = await io.slack.postMessage("Slack star", {
|
||||
text: `${payload.sender.login} starred ${payload.repository.full_name}.\nTotal: ${payload.repository.stargazers_count}⭐️`,
|
||||
channel: "C04GWUTDC3W",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-new-star",
|
||||
name: "GitHub Integration - On New Star",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewStar,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-new-repo",
|
||||
name: "GitHub Integration - On New Repository",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewRepository,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-new-branch-or-tag",
|
||||
name: "GitHub Integration - On New Branch or Tag",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewBranchOrTag,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-new-branch",
|
||||
name: "GitHub Integration - On New Branch",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewBranch,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-push",
|
||||
name: "GitHub Integration - On Push",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onPush,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-pull-request",
|
||||
name: "GitHub Integration - On Pull Request",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onPullRequest,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-pull-request-review",
|
||||
name: "GitHub Integration - On Pull Request Review",
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onPullRequestReview,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-on-pull-request-merge-commit",
|
||||
name: "GitHub Integration - on Pull Request Merge Commit",
|
||||
version: "0.1.0",
|
||||
integrations: { github },
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onPullRequest,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
|
||||
if (payload.pull_request.merged && payload.pull_request.merge_commit_sha) {
|
||||
const commit = await io.github.getCommit("get merge commit", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
commitSHA: payload.pull_request.merge_commit_sha,
|
||||
});
|
||||
await io.logger.info("Merge Commit Details", commit);
|
||||
}
|
||||
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-get-tree",
|
||||
name: "GitHub Integration - Get Tree",
|
||||
version: "0.1.0",
|
||||
integrations: { github },
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onPullRequest,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
|
||||
if (payload.pull_request.merged && payload.pull_request.merge_commit_sha) {
|
||||
const tree = await io.github.getTree("get merge commit", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
treeSHA: payload.pull_request.merge_commit_sha,
|
||||
});
|
||||
await io.logger.info("Tree ", tree);
|
||||
}
|
||||
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-get-reference",
|
||||
name: "GitHub Integration - Get Reference",
|
||||
integrations: { github },
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewBranch,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
|
||||
const ref = await io.github.getReference("Get reference", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
ref: payload.ref,
|
||||
});
|
||||
|
||||
await io.logger.info("Reference ", ref);
|
||||
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-list-matching-references",
|
||||
name: "GitHub Integration - List Matching References",
|
||||
integrations: { github },
|
||||
version: "0.1.0",
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewBranch,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
|
||||
const ref = await io.github.listMatchingReferences("List Matching References", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
ref: payload.ref,
|
||||
});
|
||||
|
||||
await io.logger.info("Reference ", ref);
|
||||
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "github-integration-get-tag",
|
||||
name: "GitHub Integration - Get Tag",
|
||||
version: "0.1.0",
|
||||
integrations: { github },
|
||||
trigger: githubApiKey.triggers.repo({
|
||||
event: events.onNewBranchOrTag,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
if (payload.ref_type === "tag") {
|
||||
const tag = io.github.getTag("Get Tag", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
tagSHA: payload.ref,
|
||||
});
|
||||
await io.logger.info("Tag ", tag);
|
||||
}
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -0,0 +1,91 @@
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import {
|
||||
ComponentDividerSpacingSize,
|
||||
ComponentTextColor,
|
||||
ComponentTextSize,
|
||||
Plain,
|
||||
} from "@trigger.dev/plain";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
const plain = new Plain({
|
||||
id: "plain-1",
|
||||
apiKey: process.env["PLAIN_API_KEY"]!,
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "plain-playground",
|
||||
name: "Plain Playground",
|
||||
version: "0.1.1",
|
||||
integrations: {
|
||||
plain,
|
||||
},
|
||||
trigger: eventTrigger({
|
||||
name: "plain.playground",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
const { customer } = await io.plain.upsertCustomer("upsert-customer", {
|
||||
identifier: {
|
||||
emailAddress: "eric@trigger.dev",
|
||||
},
|
||||
onCreate: {
|
||||
email: {
|
||||
email: "eric@trigger.dev",
|
||||
isVerified: true,
|
||||
},
|
||||
fullName: "Eric Allam",
|
||||
externalId: "123",
|
||||
},
|
||||
onUpdate: {
|
||||
fullName: {
|
||||
value: "Eric Allam",
|
||||
},
|
||||
externalId: {
|
||||
value: "123",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const foundCustomer = await io.plain.getCustomerById("get-customer", {
|
||||
customerId: customer.id,
|
||||
});
|
||||
|
||||
const timelineEntry = await io.plain.upsertCustomTimelineEntry("upsert-timeline-entry", {
|
||||
customerId: customer.id,
|
||||
title: "My timeline entry",
|
||||
components: [
|
||||
{
|
||||
componentText: {
|
||||
text: `This is a nice title`,
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: ComponentDividerSpacingSize.M,
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
textSize: ComponentTextSize.S,
|
||||
textColor: ComponentTextColor.Muted,
|
||||
text: "External id",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: foundCustomer?.externalId ?? "",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -1,8 +1,16 @@
|
||||
import { client } from "@/trigger";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { Resend } from "@trigger.dev/resend";
|
||||
import { Job, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
const resend = new Resend({
|
||||
id: "resend-client",
|
||||
apiKey: process.env.RESEND_API_KEY!,
|
||||
@@ -24,11 +32,15 @@ client.defineJob({
|
||||
resend,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.resend.sendEmail("📧", {
|
||||
const response = await io.resend.sendEmail("📧", {
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
text: payload.text,
|
||||
from: "Trigger.dev <hello@email.trigger.dev>",
|
||||
});
|
||||
|
||||
await io.logger.info("Sent email", { response });
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { TriggerClient, intervalTrigger } from "@trigger.dev/sdk";
|
||||
import { Resend } from "@trigger.dev/resend";
|
||||
import { TriggerClient, cronTrigger, intervalTrigger } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
@@ -18,7 +19,7 @@ client.defineJob({
|
||||
seconds: 60 * 3, // 3 minutes
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.runTask("task-example-1", { name: "Task 1" }, async () => {
|
||||
await io.runTask("task-example-1", async () => {
|
||||
return {
|
||||
message: "Hello World",
|
||||
};
|
||||
@@ -30,4 +31,43 @@ client.defineJob({
|
||||
},
|
||||
});
|
||||
|
||||
const resend = new Resend({
|
||||
id: "resend-client",
|
||||
apiKey: process.env.RESEND_API_KEY!,
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "weekly-kpi-report",
|
||||
name: "Weekly KPI report",
|
||||
version: "1.0.0",
|
||||
trigger: cronTrigger({
|
||||
// Every Friday at 5pm (UTC)
|
||||
cron: "0 17 * * 5",
|
||||
}),
|
||||
integrations: {
|
||||
resend,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
const kpis = await io.runTask("get-kpis", async () => {
|
||||
return await getKpiData();
|
||||
});
|
||||
|
||||
const emailList = ["jen@whatever.com", "ann@whatever.com"];
|
||||
|
||||
await io.resend.sendEmail("send-kpis", {
|
||||
to: emailList,
|
||||
subject: "Weekly KPI report",
|
||||
text: `Users: ${kpis.users}, Revenue: ${kpis.revenue}`,
|
||||
from: "Trigger.dev <hello@email.trigger.dev>",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function getKpiData() {
|
||||
return {
|
||||
users: 100_000,
|
||||
revenue: 1_000_000,
|
||||
};
|
||||
}
|
||||
|
||||
createExpressServer(client);
|
||||
|
||||
@@ -23,7 +23,7 @@ client.defineJob({
|
||||
slack,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.slack.postMessage("Slack 📝", {
|
||||
const message = await io.slack.postMessage("Slack 📝", {
|
||||
channel: "C04GWUTDC3W",
|
||||
text: "Welcome to the team, Eric!",
|
||||
});
|
||||
|
||||
@@ -38,4 +38,71 @@ client.defineJob({
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "stress-test-2",
|
||||
name: "Stress Test 2",
|
||||
version: "1.0.0",
|
||||
trigger: eventTrigger({
|
||||
name: "stress.test.2",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.runTask(`task-1`, { name: `Task 1` }, async (task) => {
|
||||
const response = await fetch("https://jsonplaceholder.typicode.com/photos");
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
await io.runTask(`task-2`, { name: `Task 2` }, async (task) => {
|
||||
const response = await fetch("https://jsonplaceholder.typicode.com/comments");
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
await io.runTask(`task-3`, { name: `Task 3` }, async (task) => {
|
||||
const response = await fetch("https://jsonplaceholder.typicode.com/photos");
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
await io.runTask(`task-4`, { name: `Task 4` }, async (task) => {
|
||||
const response = await fetch("https://jsonplaceholder.typicode.com/comments");
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
const response = await io.runTask(`task-5`, { name: `Task 5` }, async (task) => {
|
||||
const response = await fetch("https://jsonplaceholder.typicode.com/photos");
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
await io.runTask(`task-6`, { name: `Task 6` }, async (task) => {
|
||||
const response = await fetch("https://jsonplaceholder.typicode.com/users");
|
||||
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "long.running",
|
||||
name: "Long Running Job",
|
||||
version: "1.0.0",
|
||||
trigger: eventTrigger({
|
||||
name: "long.running",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// Perform X tasks in an iteration, each one taking X milliseconds
|
||||
for (let i = 0; i < payload.iterations; i++) {
|
||||
await io.runTask(`task.${i}`, { name: `Task ${i}` }, async (task) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, payload.duration ?? 5000));
|
||||
|
||||
return { i };
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
|
||||
@@ -40,30 +40,6 @@ client.defineJob({
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "stripe-example-1",
|
||||
name: "Stripe Example 1",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({
|
||||
name: "stripe.example",
|
||||
schema: z.object({
|
||||
customerId: z.string(),
|
||||
source: z.string(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
stripe,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.stripe.createCharge("create-charge", {
|
||||
amount: 100,
|
||||
currency: "usd",
|
||||
source: payload.source,
|
||||
customer: payload.customerId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "stripe-create-customer",
|
||||
name: "Stripe Create Customer",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { Typeform } from "@trigger.dev/typeform";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
export const typeform = new Typeform({
|
||||
id: "typeform-1",
|
||||
token: process.env["TYPEFORM_API_KEY"]!,
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "typeform-playground",
|
||||
name: "Typeform Playground",
|
||||
version: "0.1.1",
|
||||
integrations: {
|
||||
typeform,
|
||||
},
|
||||
trigger: eventTrigger({
|
||||
name: "typeform.playground",
|
||||
schema: z.object({
|
||||
formId: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.typeform.listForms("list-forms");
|
||||
|
||||
if (payload.formId) {
|
||||
const form = await io.typeform.getForm("get-form", {
|
||||
uid: payload.formId,
|
||||
});
|
||||
|
||||
const listResponses = await io.typeform.listResponses("list-responses", {
|
||||
uid: payload.formId,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
const allResponses = await io.typeform.getAllResponses("get-all-responses", {
|
||||
uid: payload.formId,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "typeform-webhook-2",
|
||||
name: "Typeform Webhook 2",
|
||||
version: "0.1.1",
|
||||
trigger: typeform.onFormResponse({
|
||||
uid: "KywLXMeB",
|
||||
tag: "tag1",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -1,37 +1,102 @@
|
||||
{
|
||||
"extends": "@trigger.dev/tsconfig/node18.json",
|
||||
"include": ["./src/**/*.ts"],
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"lib": ["DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
|
||||
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
|
||||
"@trigger.dev/express": ["../../packages/express/src/index"],
|
||||
"@trigger.dev/express/*": ["../../packages/express/src/*"],
|
||||
"@trigger.dev/core": ["../../packages/core/src/index"],
|
||||
"@trigger.dev/core/*": ["../../packages/core/src/*"],
|
||||
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"],
|
||||
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
|
||||
"@trigger.dev/github": ["../../integrations/github/src/index"],
|
||||
"@trigger.dev/github/*": ["../../integrations/github/src/*"],
|
||||
"@trigger.dev/slack": ["../../integrations/slack/src/index"],
|
||||
"@trigger.dev/slack/*": ["../../integrations/slack/src/*"],
|
||||
"@trigger.dev/openai": ["../../integrations/openai/src/index"],
|
||||
"@trigger.dev/openai/*": ["../../integrations/openai/src/*"],
|
||||
"@trigger.dev/resend": ["../../integrations/resend/src/index"],
|
||||
"@trigger.dev/resend/*": ["../../integrations/resend/src/*"],
|
||||
"@trigger.dev/typeform": ["../../integrations/typeform/src/index"],
|
||||
"@trigger.dev/typeform/*": ["../../integrations/typeform/src/*"],
|
||||
"@trigger.dev/plain": ["../../integrations/plain/src/index"],
|
||||
"@trigger.dev/plain/*": ["../../integrations/plain/src/*"],
|
||||
"@trigger.dev/supabase": ["../../integrations/supabase/src/index"],
|
||||
"@trigger.dev/supabase/*": ["../../integrations/supabase/src/*"],
|
||||
"@trigger.dev/stripe": ["../../integrations/stripe/src/index"],
|
||||
"@trigger.dev/stripe/*": ["../../integrations/stripe/src/*"],
|
||||
"@trigger.dev/sendgrid": ["../../integrations/sendgrid/src/index"],
|
||||
"@trigger.dev/sendgrid/*": ["../../integrations/sendgrid/src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@trigger.dev/sdk": [
|
||||
"../../packages/trigger-sdk/src/index"
|
||||
],
|
||||
"@trigger.dev/sdk/*": [
|
||||
"../../packages/trigger-sdk/src/*"
|
||||
],
|
||||
"@trigger.dev/express": [
|
||||
"../../packages/express/src/index"
|
||||
],
|
||||
"@trigger.dev/express/*": [
|
||||
"../../packages/express/src/*"
|
||||
],
|
||||
"@trigger.dev/core": [
|
||||
"../../packages/core/src/index"
|
||||
],
|
||||
"@trigger.dev/core/*": [
|
||||
"../../packages/core/src/*"
|
||||
],
|
||||
"@trigger.dev/integration-kit": [
|
||||
"../../packages/integration-kit/src/index"
|
||||
],
|
||||
"@trigger.dev/integration-kit/*": [
|
||||
"../../packages/integration-kit/src/*"
|
||||
],
|
||||
"@trigger.dev/github": [
|
||||
"../../integrations/github/src/index"
|
||||
],
|
||||
"@trigger.dev/github/*": [
|
||||
"../../integrations/github/src/*"
|
||||
],
|
||||
"@trigger.dev/slack": [
|
||||
"../../integrations/slack/src/index"
|
||||
],
|
||||
"@trigger.dev/slack/*": [
|
||||
"../../integrations/slack/src/*"
|
||||
],
|
||||
"@trigger.dev/openai": [
|
||||
"../../integrations/openai/src/index"
|
||||
],
|
||||
"@trigger.dev/openai/*": [
|
||||
"../../integrations/openai/src/*"
|
||||
],
|
||||
"@trigger.dev/resend": [
|
||||
"../../integrations/resend/src/index"
|
||||
],
|
||||
"@trigger.dev/resend/*": [
|
||||
"../../integrations/resend/src/*"
|
||||
],
|
||||
"@trigger.dev/typeform": [
|
||||
"../../integrations/typeform/src/index"
|
||||
],
|
||||
"@trigger.dev/typeform/*": [
|
||||
"../../integrations/typeform/src/*"
|
||||
],
|
||||
"@trigger.dev/plain": [
|
||||
"../../integrations/plain/src/index"
|
||||
],
|
||||
"@trigger.dev/plain/*": [
|
||||
"../../integrations/plain/src/*"
|
||||
],
|
||||
"@trigger.dev/supabase": [
|
||||
"../../integrations/supabase/src/index"
|
||||
],
|
||||
"@trigger.dev/supabase/*": [
|
||||
"../../integrations/supabase/src/*"
|
||||
],
|
||||
"@trigger.dev/stripe": [
|
||||
"../../integrations/stripe/src/index"
|
||||
],
|
||||
"@trigger.dev/stripe/*": [
|
||||
"../../integrations/stripe/src/*"
|
||||
],
|
||||
"@trigger.dev/sendgrid": [
|
||||
"../../integrations/sendgrid/src/index"
|
||||
],
|
||||
"@trigger.dev/sendgrid/*": [
|
||||
"../../integrations/sendgrid/src/*"
|
||||
],
|
||||
"@trigger.dev/airtable": [
|
||||
"../../integrations/airtable/src/index"
|
||||
],
|
||||
"@trigger.dev/airtable/*": [
|
||||
"../../integrations/airtable/src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { client, github, githubUser, openai, slack } from "@/trigger";
|
||||
import { events } from "@trigger.dev/github";
|
||||
import {
|
||||
DynamicSchedule,
|
||||
DynamicTrigger,
|
||||
Job,
|
||||
cronTrigger,
|
||||
eventTrigger,
|
||||
@@ -13,169 +11,8 @@ import {
|
||||
} from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, {
|
||||
id: "github-issue-opened",
|
||||
event: events.onIssueOpened,
|
||||
source: github.sources.repo,
|
||||
});
|
||||
|
||||
const dynamicUserTrigger = new DynamicTrigger(client, {
|
||||
id: "dynamic-user-trigger",
|
||||
event: events.onIssueOpened,
|
||||
source: githubUser.sources.repo,
|
||||
});
|
||||
|
||||
const dynamicSchedule = new DynamicSchedule(client, {
|
||||
id: "dynamic-interval",
|
||||
});
|
||||
|
||||
const enabled = true;
|
||||
|
||||
client.defineJob({
|
||||
id: "test-background-fetch-retry",
|
||||
name: "Test background fetch retry",
|
||||
version: "0.0.1",
|
||||
enabled,
|
||||
trigger: eventTrigger({
|
||||
name: "test.background-fetch",
|
||||
schema: z.object({
|
||||
url: z.string(),
|
||||
method: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
body: z.any().optional(),
|
||||
retry: z.any().optional(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
return await io.backgroundFetch<any>(
|
||||
"fetch",
|
||||
payload.url,
|
||||
{
|
||||
method: payload.method ?? "GET",
|
||||
headers: payload.headers,
|
||||
body: payload.body ? JSON.stringify(payload.body) : undefined,
|
||||
},
|
||||
payload.retry
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const CHAT_MODELS = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-0301",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-4",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-0613",
|
||||
];
|
||||
|
||||
client.defineJob({
|
||||
id: "openai-test",
|
||||
name: "OpenAI Test",
|
||||
version: "0.0.1",
|
||||
enabled,
|
||||
trigger: eventTrigger({
|
||||
name: "openai.test",
|
||||
schema: z.object({
|
||||
model: z.string(),
|
||||
prompt: z.string(),
|
||||
background: z.boolean().optional(),
|
||||
}),
|
||||
}),
|
||||
integrations: {
|
||||
openai,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
if (CHAT_MODELS.includes(payload.model)) {
|
||||
if (payload.background) {
|
||||
const completion = await io.openai.backgroundCreateChatCompletion("✨", {
|
||||
model: payload.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: payload.prompt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return completion;
|
||||
}
|
||||
|
||||
const completion = await io.openai.createChatCompletion("✨", {
|
||||
model: payload.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: payload.prompt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return completion;
|
||||
}
|
||||
|
||||
if (payload.background) {
|
||||
const completion = await io.openai.backgroundCreateCompletion("✨", {
|
||||
model: payload.model,
|
||||
prompt: payload.prompt,
|
||||
});
|
||||
|
||||
return completion;
|
||||
}
|
||||
|
||||
const completion = await io.openai.createCompletion("✨", {
|
||||
model: payload.model,
|
||||
prompt: payload.prompt,
|
||||
});
|
||||
|
||||
return completion;
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "openai-errors",
|
||||
name: "OpenAI Errors",
|
||||
version: "0.0.1",
|
||||
enabled,
|
||||
trigger: eventTrigger({
|
||||
name: "openai.errors",
|
||||
}),
|
||||
integrations: {
|
||||
openai,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.openai.createChatCompletion("chat-completion", {
|
||||
model: "gpt-3.5-turbo",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are an AI assistant that is helpful, creative, clever, and very friendly.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: "Call the supplied function that will tweet a really funny joke",
|
||||
},
|
||||
],
|
||||
function_call: { name: "tweetFunnyJoke" },
|
||||
functions: [
|
||||
{
|
||||
name: "tweetFunnyJoke",
|
||||
description:
|
||||
"Tweets a really funny joke. The joke is so funny that it will make you laugh out loud.",
|
||||
parameters: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "on-missing-auth-connection",
|
||||
name: "On missing auth connection",
|
||||
@@ -232,25 +69,6 @@ client.defineJob({
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "user-on-issue-opened",
|
||||
name: "user on issue opened",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: dynamicUserTrigger,
|
||||
integrations: {
|
||||
github: githubUser,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("user-on-issue-opened", { ctx });
|
||||
|
||||
return await io.github.getRepo("get.repo", {
|
||||
owner: payload.repository.owner.login,
|
||||
repo: payload.repository.name,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "get-user-repo",
|
||||
name: "Get User Repo",
|
||||
@@ -271,88 +89,6 @@ client.defineJob({
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "get-user-repo-on-schedule",
|
||||
name: "Get User Repo On Schedule",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: dynamicSchedule,
|
||||
integrations: {
|
||||
github: githubUser,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
return await io.github.getRepo("get.repo", {
|
||||
owner: ctx.event.context.source.metadata.owner,
|
||||
repo: ctx.event.context.source.metadata.repo,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "register-dynamic-interval",
|
||||
name: "Register Dynamic Interval",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: eventTrigger({
|
||||
name: "dynamic.interval",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
seconds: z.number().int().positive(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.registerInterval("📆", dynamicSchedule, payload.id, {
|
||||
seconds: payload.seconds,
|
||||
});
|
||||
|
||||
await io.wait("wait", 60);
|
||||
|
||||
await io.unregisterInterval("❌📆", dynamicSchedule, payload.id);
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "register-dynamic-cron",
|
||||
name: "Register Dynamic Cron",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: eventTrigger({
|
||||
name: "dynamic.cron",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
cron: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.registerCron("📆", dynamicSchedule, payload.id, {
|
||||
cron: payload.cron,
|
||||
});
|
||||
|
||||
await io.wait("wait", 60);
|
||||
|
||||
await io.unregisterCron("❌📆", dynamicSchedule, payload.id);
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "use-dynamic-interval",
|
||||
name: "Use Dynamic Interval",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: dynamicSchedule,
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.wait("wait", 5); // wait for 5 seconds
|
||||
await io.logger.info("This is a log info message", {
|
||||
payload,
|
||||
});
|
||||
await io.sendEvent("send-event", {
|
||||
name: "custom.event",
|
||||
payload,
|
||||
context: ctx,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "event-1",
|
||||
name: "Run when the foo.bar event happens",
|
||||
@@ -364,15 +100,19 @@ client.defineJob({
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.try(
|
||||
async () => {
|
||||
return await io.runTask("task-1", { name: "task-1", retry: { limit: 3 } }, async (task) => {
|
||||
if (task.attempts > 2) {
|
||||
return {
|
||||
bar: "foo",
|
||||
};
|
||||
}
|
||||
return await io.runTask(
|
||||
"task-1",
|
||||
async (task) => {
|
||||
if (task.attempts > 2) {
|
||||
return {
|
||||
bar: "foo",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Task failed on ${task.attempts} attempt(s)`);
|
||||
});
|
||||
throw new Error(`Task failed on ${task.attempts} attempt(s)`);
|
||||
},
|
||||
{ name: "task-1", retry: { limit: 3 } }
|
||||
);
|
||||
},
|
||||
async (error) => {
|
||||
// These should never be reached
|
||||
@@ -390,9 +130,13 @@ client.defineJob({
|
||||
);
|
||||
|
||||
try {
|
||||
await io.runTask("task-2", { name: "task-2", retry: { limit: 5 } }, async (task) => {
|
||||
throw new Error(`Task failed on ${task.attempts} attempt(s)`);
|
||||
});
|
||||
await io.runTask(
|
||||
"task-2",
|
||||
async (task) => {
|
||||
throw new Error(`Task failed on ${task.attempts} attempt(s)`);
|
||||
},
|
||||
{ name: "task-2", retry: { limit: 5 } }
|
||||
);
|
||||
} catch (error) {
|
||||
if (isTriggerError(error)) {
|
||||
throw error;
|
||||
@@ -433,57 +177,21 @@ client.defineJob({
|
||||
context: ctx,
|
||||
});
|
||||
|
||||
await io.runTask(
|
||||
"level 1",
|
||||
{
|
||||
name: "Level 1",
|
||||
},
|
||||
async () => {
|
||||
await io.runTask(
|
||||
"level 2",
|
||||
{
|
||||
name: "Level 2",
|
||||
},
|
||||
async () => {
|
||||
await io.runTask(
|
||||
"level 3",
|
||||
{
|
||||
name: "Level 3",
|
||||
},
|
||||
async () => {
|
||||
await io.runTask(
|
||||
"level 4",
|
||||
{
|
||||
name: "Level 4",
|
||||
},
|
||||
async () => {
|
||||
await io.runTask(
|
||||
"level 5",
|
||||
{
|
||||
name: "Level 5",
|
||||
},
|
||||
async () => {}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
await io.runTask("level 1", async () => {
|
||||
await io.runTask("level 2", async () => {
|
||||
await io.runTask("level 3", async () => {
|
||||
await io.runTask("level 4", async () => {
|
||||
await io.runTask("level 5", async () => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await io.wait("5 minutes", 5 * 60);
|
||||
|
||||
await io.runTask(
|
||||
"Fingers crossed",
|
||||
{
|
||||
name: "Just a task 🤞",
|
||||
},
|
||||
async () => {
|
||||
throw new Error("You messed up buddy!");
|
||||
}
|
||||
);
|
||||
await io.runTask("Fingers crossed", async () => {
|
||||
throw new Error("You messed up buddy!");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -529,76 +237,6 @@ client.defineJob({
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "register-dynamic-trigger-on-new-repo",
|
||||
name: "Register dynamic trigger on new repo",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: eventTrigger({
|
||||
name: "new.repo",
|
||||
schema: z.object({ owner: z.string(), repo: z.string() }),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
return await io.registerTrigger("register-repo", dynamicOnIssueOpenedTrigger, payload.repo, {
|
||||
owner: payload.owner,
|
||||
repo: payload.repo,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "listen-for-dynamic-trigger",
|
||||
name: "Listen for dynamic trigger",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: dynamicOnIssueOpenedTrigger,
|
||||
integrations: {
|
||||
slack,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.slack.postMessage("Slack 📝", {
|
||||
text: `New Issue opened on dynamically triggered repo: ${
|
||||
payload.issue.html_url
|
||||
}. \n\n${JSON.stringify(ctx)}`,
|
||||
channel: "C04GWUTDC3W",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "listen-for-dynamic-trigger-2",
|
||||
name: "Listen for dynamic trigger-2",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: dynamicOnIssueOpenedTrigger,
|
||||
integrations: {
|
||||
slack,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.slack.postMessage("Slack 📝", {
|
||||
text: `New Issue opened on dynamically triggered repo 2: ${payload.issue.html_url}`,
|
||||
channel: "C04GWUTDC3W",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "listen-for-dynamic-trigger-3",
|
||||
name: "Listen for dynamic trigger-3",
|
||||
version: "0.1.1",
|
||||
enabled,
|
||||
trigger: dynamicOnIssueOpenedTrigger,
|
||||
integrations: {
|
||||
slack,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.slack.postMessage("Slack 📝", {
|
||||
text: `New Issue opened on dynamically triggered repo 3: ${payload.issue.html_url}`,
|
||||
channel: "C04GWUTDC3W",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "alert-on-new-github-issues-3",
|
||||
name: "Alert on new GitHub issues",
|
||||
@@ -613,7 +251,7 @@ client.defineJob({
|
||||
repo: "basic-starter-12k",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.runTask("slow task", { name: "slow task" }, async () => {
|
||||
await io.runTask("slow task", async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0`
|
||||
- `@trigger.dev/sdk@2.1.0`
|
||||
|
||||
## 2.1.0-beta.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.1`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.1`
|
||||
|
||||
## 2.1.0-beta.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.0`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.0`
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# @trigger.dev/airtable
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "2.1.0",
|
||||
"description": "Trigger.dev integration for airtable",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.d.ts",
|
||||
"dist/index.js.map"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/node": "16.x",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsup": "7.1.x",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && npm run build:tsup",
|
||||
"build:tsup": "tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.0",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { DisplayProperty, IOWithIntegrations, IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { FieldSet, Records, SelectOptions } from "airtable";
|
||||
import { AirtableFieldSet, AirtableRecord, AirtableRunTask, CreateAirtableRecord } from ".";
|
||||
import { QueryParams } from "airtable/lib/query_params";
|
||||
|
||||
type TableParams<Params extends Record<string, unknown>> = {
|
||||
tableName: string;
|
||||
} & Params;
|
||||
|
||||
export type AirtableRecordsParams = TableParams<{}>;
|
||||
export type AirtableRecords = Records<FieldSet>;
|
||||
|
||||
export class Base {
|
||||
runTask: AirtableRunTask;
|
||||
baseId: string;
|
||||
|
||||
constructor(runTask: AirtableRunTask, baseId: string) {
|
||||
this.runTask = runTask;
|
||||
this.baseId = baseId;
|
||||
}
|
||||
|
||||
table<TFields extends AirtableFieldSet>(tableName: string) {
|
||||
return new Table<TFields>(this.runTask, this.baseId, tableName);
|
||||
}
|
||||
}
|
||||
|
||||
export class Table<TFields extends AirtableFieldSet> {
|
||||
runTask: AirtableRunTask;
|
||||
baseId: string;
|
||||
tableName: string;
|
||||
|
||||
constructor(runTask: AirtableRunTask, baseId: string, tableName: string) {
|
||||
this.runTask = runTask;
|
||||
this.baseId = baseId;
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
getRecords(key: IntegrationTaskKey, params?: SelectOptions<TFields>) {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const result = await client
|
||||
.base(this.baseId)
|
||||
.table<TFields>(this.tableName)
|
||||
.select(params)
|
||||
.all();
|
||||
return result.map((record) => toSerializableRecord<TFields>(record));
|
||||
},
|
||||
{
|
||||
name: "Get Records",
|
||||
params,
|
||||
properties: [...tableParams({ baseId: this.baseId, tableName: this.tableName })],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getRecord(key: IntegrationTaskKey, recordId: string) {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const result = await client.base(this.baseId).table<TFields>(this.tableName).find(recordId);
|
||||
return toSerializableRecord<TFields>(result);
|
||||
},
|
||||
{
|
||||
name: "Get Record",
|
||||
params: { recordId },
|
||||
properties: [
|
||||
...tableParams({ baseId: this.baseId, tableName: this.tableName }),
|
||||
{ label: "Record", text: recordId },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createRecords(key: IntegrationTaskKey, records: { fields: Partial<TFields> }[]) {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const result = await client
|
||||
.base(this.baseId)
|
||||
.table<TFields>(this.tableName)
|
||||
.create(records);
|
||||
return result.map((record) => toSerializableRecord<TFields>(record));
|
||||
},
|
||||
{
|
||||
name: "Create Records",
|
||||
params: records,
|
||||
properties: [
|
||||
...tableParams({ baseId: this.baseId, tableName: this.tableName }),
|
||||
{ label: "Created records", text: records.length.toString() },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateRecords(key: IntegrationTaskKey, records: { id: string; fields: Partial<TFields> }[]) {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const result = await client
|
||||
.base(this.baseId)
|
||||
.table<TFields>(this.tableName)
|
||||
.update(records);
|
||||
return result.map((record) => toSerializableRecord<TFields>(record));
|
||||
},
|
||||
{
|
||||
name: "Update Records",
|
||||
params: records,
|
||||
properties: [
|
||||
...tableParams({ baseId: this.baseId, tableName: this.tableName }),
|
||||
{ label: "Updated records", text: records.length.toString() },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteRecords(key: IntegrationTaskKey, recordIds: string[]) {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const result = await client
|
||||
.base(this.baseId)
|
||||
.table<TFields>(this.tableName)
|
||||
.destroy(recordIds);
|
||||
return result.map((record) => toSerializableRecord<TFields>(record));
|
||||
},
|
||||
{
|
||||
name: "Delete Records",
|
||||
params: { recordIds },
|
||||
properties: [
|
||||
...tableParams({ baseId: this.baseId, tableName: this.tableName }),
|
||||
{ label: "Deleted records", text: recordIds.length.toString() },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toSerializableRecord<TFields extends AirtableFieldSet>(record: AirtableRecord<TFields>) {
|
||||
return {
|
||||
id: record.id,
|
||||
fields: record.fields,
|
||||
commentCount: record.commentCount,
|
||||
} as AirtableRecord<TFields>;
|
||||
}
|
||||
|
||||
function tableParams(params: { baseId: string; tableName: string }): DisplayProperty[] {
|
||||
return [
|
||||
{
|
||||
label: "Base",
|
||||
text: params.baseId,
|
||||
},
|
||||
{
|
||||
label: "Table",
|
||||
text: params.tableName,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { EventSpecification } from "@trigger.dev/sdk";
|
||||
import { WebhookPayload } from "./schemas";
|
||||
|
||||
type OnTableChanged = WebhookPayload;
|
||||
|
||||
export const onTableChanged: EventSpecification<OnTableChanged> = {
|
||||
name: "changed",
|
||||
title: "On Table Changed",
|
||||
source: "airtable.com",
|
||||
icon: "airtable",
|
||||
//todo properties with base and table (if there is one), maybe other specs too
|
||||
// properties: [
|
||||
//todo: add a payload example
|
||||
// examples: [
|
||||
// {
|
||||
// id: "recurring",
|
||||
// name: "Recurring Price",
|
||||
// icon: "airtable",
|
||||
// payload: {
|
||||
// id: "price_1NYV6vI0XSgju2urKsSmI53v",
|
||||
// object: "price",
|
||||
// active: true,
|
||||
// billing_scheme: "per_unit",
|
||||
// created: 1690467853,
|
||||
// currency: "usd",
|
||||
// custom_unit_amount: null,
|
||||
// livemode: false,
|
||||
// lookup_key: null,
|
||||
// metadata: {},
|
||||
// nickname: null,
|
||||
// product: "prod_OLBTh0QPxDXkIU",
|
||||
// recurring: {
|
||||
// aggregate_usage: null,
|
||||
// interval: "month",
|
||||
// interval_count: 1,
|
||||
// trial_period_days: null,
|
||||
// usage_type: "licensed",
|
||||
// },
|
||||
// tax_behavior: "unspecified",
|
||||
// tiers_mode: null,
|
||||
// transform_quantity: null,
|
||||
// type: "recurring",
|
||||
// unit_amount: 1500,
|
||||
// unit_amount_decimal: "1500",
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
parsePayload: (payload) => payload as OnTableChanged,
|
||||
runProperties: (payload) => [{ label: "Change source", text: payload.actionMetadata.source }],
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Prettify } from "@trigger.dev/integration-kit";
|
||||
import {
|
||||
Json,
|
||||
type ConnectionAuth,
|
||||
type IO,
|
||||
type IOTask,
|
||||
type IntegrationTaskKey,
|
||||
type RunTaskErrorCallback,
|
||||
type RunTaskOptions,
|
||||
type TriggerIntegration,
|
||||
retry,
|
||||
} from "@trigger.dev/sdk";
|
||||
import AirtableSDK from "airtable";
|
||||
import { Base } from "./base";
|
||||
import * as events from "./events";
|
||||
import {
|
||||
WebhookChangeType,
|
||||
WebhookDataType,
|
||||
Webhooks,
|
||||
createTrigger,
|
||||
createWebhookEventSource,
|
||||
} from "./webhooks";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
export type AirtableIntegrationOptions = {
|
||||
/** An ID for this client */
|
||||
id: string;
|
||||
/** Use this if you pass in a [Personal Access Token](https://airtable.com/developers/web/guides/personal-access-tokens). If omitted, it will use OAuth. */
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export type AirtableRunTask = InstanceType<typeof Airtable>["runTask"];
|
||||
|
||||
export class Airtable implements TriggerIntegration {
|
||||
private _options: AirtableIntegrationOptions;
|
||||
private _client?: AirtableSDK;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
constructor(options: Prettify<AirtableIntegrationOptions>) {
|
||||
if (Object.keys(options).includes("token") && !options.token) {
|
||||
throw `Can't create Airtable integration (${options.id}) as token was passed in but undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
return this._options.token ? ("LOCAL" as const) : ("HOSTED" as const);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "airtable", name: "Airtable" };
|
||||
}
|
||||
|
||||
get source() {
|
||||
return createWebhookEventSource(this);
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const airtable = new Airtable(this._options);
|
||||
airtable._io = io;
|
||||
airtable._connectionKey = connectionKey;
|
||||
airtable._client = this.createClient(auth);
|
||||
return airtable;
|
||||
}
|
||||
|
||||
createClient(auth?: ConnectionAuth) {
|
||||
if (auth) {
|
||||
return new AirtableSDK({
|
||||
apiKey: auth.accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._options.token) {
|
||||
return new AirtableSDK({
|
||||
apiKey: this._options.token,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("No auth");
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: AirtableSDK, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "airtable",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
|
||||
base(baseId: string) {
|
||||
return new Base(this.runTask.bind(this), baseId);
|
||||
}
|
||||
|
||||
//todo these require batch support because they send too many events
|
||||
// onTableChanges(params: {
|
||||
// baseId: string;
|
||||
// tableId?: string;
|
||||
// changeTypes?: WebhookChangeType[];
|
||||
// dataTypes?: WebhookDataType[];
|
||||
// }) {
|
||||
// return createTrigger(this.source, events.onTableChanged, params, {
|
||||
// changeTypes: params.changeTypes,
|
||||
// dataTypes: ["tableData", "tableFields", "tableMetadata"],
|
||||
// });
|
||||
// }
|
||||
|
||||
webhooks() {
|
||||
return new Webhooks(this.runTask.bind(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const UserSourceMetadata = z.object({
|
||||
user: z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
permissionLevel: z.union([
|
||||
z.literal("none"),
|
||||
z.literal("read"),
|
||||
z.literal("comment"),
|
||||
z.literal("edit"),
|
||||
z.literal("create"),
|
||||
]),
|
||||
name: z.string().optional(),
|
||||
profilePicUrl: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const WebhookAction = z.discriminatedUnion("source", [
|
||||
z.object({
|
||||
source: z.literal("client"),
|
||||
sourceMetadata: UserSourceMetadata,
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("publicApi"),
|
||||
sourceMetadata: UserSourceMetadata,
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("formSubmission"),
|
||||
sourceMetadata: z.object({
|
||||
viewId: z.string(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("automation"),
|
||||
sourceMetadata: z.object({
|
||||
automationId: z.string(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("system"),
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("sync"),
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("anonymousUser"),
|
||||
}),
|
||||
z.object({
|
||||
source: z.literal("unknown"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const CreatedFieldSchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.union([
|
||||
z.literal("singleLineText"),
|
||||
z.literal("email"),
|
||||
z.literal("url"),
|
||||
z.literal("multilineText"),
|
||||
z.literal("number"),
|
||||
z.literal("percent"),
|
||||
z.literal("currency"),
|
||||
z.literal("singleSelect"),
|
||||
z.literal("multipleSelects"),
|
||||
z.literal("singleCollaborator"),
|
||||
z.literal("multipleCollaborators"),
|
||||
z.literal("multipleRecordLinks"),
|
||||
z.literal("date"),
|
||||
z.literal("dateTime"),
|
||||
z.literal("phoneNumber"),
|
||||
z.literal("multipleAttachments"),
|
||||
z.literal("checkbox"),
|
||||
z.literal("formula"),
|
||||
z.literal("createdTime"),
|
||||
z.literal("rollup"),
|
||||
z.literal("count"),
|
||||
z.literal("lookup"),
|
||||
z.literal("multipleLookupValues"),
|
||||
z.literal("autoNumber"),
|
||||
z.literal("barcode"),
|
||||
z.literal("rating"),
|
||||
z.literal("richText"),
|
||||
z.literal("duration"),
|
||||
z.literal("lastModifiedTime"),
|
||||
z.literal("button"),
|
||||
z.literal("createdBy"),
|
||||
z.literal("lastModifiedBy"),
|
||||
z.literal("externalSyncSource"),
|
||||
z.literal("aiText"),
|
||||
z.string(),
|
||||
]),
|
||||
});
|
||||
|
||||
const ChangedRecordFieldSchema = z.object({
|
||||
cellValuesByFieldId: z.record(z.any()),
|
||||
});
|
||||
|
||||
const CreatedRecordSchema = ChangedRecordFieldSchema.and(
|
||||
z.object({
|
||||
createdTime: z.string(),
|
||||
})
|
||||
);
|
||||
|
||||
const ChangedRecordSchema = z.object({
|
||||
current: ChangedRecordFieldSchema,
|
||||
previous: ChangedRecordFieldSchema.optional(),
|
||||
unchanged: ChangedRecordFieldSchema.optional(),
|
||||
});
|
||||
|
||||
const ChangedTableMetadata = z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().nullish(),
|
||||
});
|
||||
|
||||
const ChangedTableSchema = z.object({
|
||||
changedViewsById: z
|
||||
.record(
|
||||
z.object({
|
||||
changedRecordsById: z.record(ChangedRecordSchema).optional(),
|
||||
createdRecordsById: z.record(CreatedRecordSchema).optional(),
|
||||
destroyedRecordIds: z.array(z.string()).optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
changedFieldsById: z
|
||||
.record(
|
||||
z.object({
|
||||
current: CreatedFieldSchema.partial(),
|
||||
previous: CreatedFieldSchema.partial().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
changedRecordsById: z.record(ChangedRecordSchema).optional(),
|
||||
createdFieldsById: z.record(CreatedFieldSchema).optional(),
|
||||
createdRecordsById: z.record(CreatedRecordSchema).optional(),
|
||||
changedMetadata: z
|
||||
.object({
|
||||
current: ChangedTableMetadata,
|
||||
previous: ChangedTableMetadata.optional(),
|
||||
})
|
||||
.optional(),
|
||||
destroyedFieldIds: z.array(z.string()).optional(),
|
||||
destroyedRecordIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const CreatedTableSchema = z.object({
|
||||
metadata: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
fieldsById: z.record(CreatedFieldSchema).optional(),
|
||||
recordsById: z.record(CreatedRecordSchema).optional(),
|
||||
});
|
||||
|
||||
export const WebhookPayloadSchema = z.object({
|
||||
timestamp: z.coerce.date(),
|
||||
baseTransactionNumber: z.number(),
|
||||
payloadFormat: z.literal("v0"),
|
||||
actionMetadata: WebhookAction,
|
||||
changedTablesById: z.record(ChangedTableSchema).optional(),
|
||||
createdTablesById: z.record(CreatedTableSchema).optional(),
|
||||
destroyedTableIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>;
|
||||
|
||||
export const ListWebhooksResponseSchema = z.object({
|
||||
cursor: z.number(),
|
||||
mightHaveMore: z.boolean(),
|
||||
payloads: z.array(WebhookPayloadSchema),
|
||||
});
|
||||
|
||||
export type ListWebhooksResponse = z.infer<typeof ListWebhooksResponseSchema>;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Prettify } from "@trigger.dev/integration-kit";
|
||||
|
||||
export type AirtableFieldSet = {
|
||||
[key: string]:
|
||||
| undefined
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Collaborator
|
||||
| Collaborator[]
|
||||
| string[]
|
||||
| Attachment[];
|
||||
};
|
||||
|
||||
export type Collaborator = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Attachment = {
|
||||
id: string;
|
||||
url: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
type: string;
|
||||
thumbnails?: {
|
||||
small: Thumbnail;
|
||||
large: Thumbnail;
|
||||
full: Thumbnail;
|
||||
};
|
||||
};
|
||||
|
||||
export type Thumbnail = {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type AirtableRecord<TFields extends AirtableFieldSet> = {
|
||||
id: string;
|
||||
fields: TFields;
|
||||
commentCount?: number;
|
||||
};
|
||||
|
||||
export type CreateAirtableRecord<TFields extends AirtableFieldSet> = Pick<
|
||||
AirtableRecord<Partial<TFields>>,
|
||||
"fields"
|
||||
>;
|
||||
@@ -0,0 +1,460 @@
|
||||
import {
|
||||
EventFilter,
|
||||
ExternalSource,
|
||||
ExternalSourceTrigger,
|
||||
HandlerEvent,
|
||||
IntegrationTaskKey,
|
||||
Logger,
|
||||
} from "@trigger.dev/sdk";
|
||||
import AirtableSDK from "airtable";
|
||||
import { z } from "zod";
|
||||
import * as events from "./events";
|
||||
import { Airtable, AirtableRunTask } from "./index";
|
||||
import { ListWebhooksResponse, ListWebhooksResponseSchema } from "./schemas";
|
||||
|
||||
const WebhookFromSourceSchema = z.union([
|
||||
z.literal("formSubmission"),
|
||||
z.literal("client"),
|
||||
z.literal("anonymousUser"),
|
||||
// we don't currently support these as they can cause feedback loops
|
||||
// z.literal("publicApi"),
|
||||
// z.literal("automation"),
|
||||
// z.literal("system"),
|
||||
// z.literal("sync"),
|
||||
// z.literal("unknown"),
|
||||
]);
|
||||
|
||||
type WebhookFromSource = z.infer<typeof WebhookFromSourceSchema>;
|
||||
const WebhookDataTypeSchema = z.union([
|
||||
z.literal("tableData"),
|
||||
z.literal("tableFields"),
|
||||
z.literal("tableMetadata"),
|
||||
]);
|
||||
export type WebhookDataType = z.infer<typeof WebhookDataTypeSchema>;
|
||||
const WebhookChangeTypeSchema = z.union([
|
||||
z.literal("add"),
|
||||
z.literal("remove"),
|
||||
z.literal("update"),
|
||||
]);
|
||||
export type WebhookChangeType = z.infer<typeof WebhookChangeTypeSchema>;
|
||||
type WebhookSpecification = {
|
||||
filters: {
|
||||
dataTypes: WebhookDataType[];
|
||||
recordChangeScope?: string;
|
||||
changeTypes?: WebhookChangeType[];
|
||||
fromSources?: WebhookFromSource[];
|
||||
};
|
||||
};
|
||||
|
||||
const apiUrl = "https://api.airtable.com/v0/bases";
|
||||
|
||||
export class Webhooks {
|
||||
runTask: AirtableRunTask;
|
||||
|
||||
constructor(runTask: AirtableRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
{ baseId, url, options }: { baseId: string; url: string; options: WebhookSpecification }
|
||||
): Promise<WebhookRegistrationData> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
// create webhook
|
||||
const response = await fetch(`${apiUrl}/${baseId}/webhooks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${client._apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
notificationUrl: url,
|
||||
specification: {
|
||||
options: {
|
||||
...options,
|
||||
includes: {
|
||||
includePreviousCellValues: true,
|
||||
includePreviousFieldDefinitions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response
|
||||
.text()
|
||||
.then((t) => t)
|
||||
.catch((e) => "No body");
|
||||
|
||||
throw new Error(
|
||||
`Failed to create webhook: ${response.status} ${response.statusText}\n${errorText}`
|
||||
);
|
||||
}
|
||||
|
||||
const webhook = await response.json();
|
||||
const parsed = WebhookRegistrationDataSchema.parse(webhook);
|
||||
return parsed;
|
||||
},
|
||||
{
|
||||
name: "Create webhook",
|
||||
params: {
|
||||
baseId,
|
||||
url,
|
||||
options,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
list(key: IntegrationTaskKey, { baseId }: { baseId: string }): Promise<WebhookListData> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
// create webhook
|
||||
const response = await fetch(`${apiUrl}/${baseId}/webhooks`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${client._apiKey}`,
|
||||
},
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list webhooks: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const webhook = await response.json();
|
||||
const parsed = WebhookListDataSchema.parse(webhook);
|
||||
return parsed;
|
||||
},
|
||||
{
|
||||
name: "List webhooks",
|
||||
params: {
|
||||
baseId,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
delete(key: IntegrationTaskKey, { baseId, webhookId }: { baseId: string; webhookId: string }) {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
// create webhook
|
||||
const response = await fetch(`${apiUrl}/${baseId}/webhooks/${webhookId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${client._apiKey}`,
|
||||
},
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete webhook: ${response.statusText}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Delete webhook",
|
||||
params: {
|
||||
baseId,
|
||||
webhookId,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async update(
|
||||
key: IntegrationTaskKey,
|
||||
{
|
||||
baseId,
|
||||
url,
|
||||
webhookId,
|
||||
options,
|
||||
}: { baseId: string; url: string; webhookId: string; options: WebhookSpecification }
|
||||
) {
|
||||
await this.delete(`${key}-delete`, { baseId, webhookId });
|
||||
return await this.create(`${key}-create`, { baseId, url, options });
|
||||
}
|
||||
}
|
||||
|
||||
type AirtableEvents = (typeof events)[keyof typeof events];
|
||||
|
||||
export type TriggerParams = {
|
||||
baseId: string;
|
||||
filter?: EventFilter;
|
||||
};
|
||||
|
||||
type CreateTriggersResult<TEventSpecification extends AirtableEvents> = ExternalSourceTrigger<
|
||||
TEventSpecification,
|
||||
ReturnType<typeof createWebhookEventSource>
|
||||
>;
|
||||
|
||||
export function createTrigger<TEventSpecification extends AirtableEvents>(
|
||||
source: ReturnType<typeof createWebhookEventSource>,
|
||||
event: TEventSpecification,
|
||||
params: TriggerParams,
|
||||
options: {
|
||||
dataTypes: WebhookDataType[];
|
||||
changeTypes?: WebhookChangeType[];
|
||||
fromSources?: WebhookFromSource[];
|
||||
}
|
||||
): CreateTriggersResult<TEventSpecification> {
|
||||
return new ExternalSourceTrigger({
|
||||
event,
|
||||
params,
|
||||
source,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
const WebhookRegistrationDataSchema = z.object({
|
||||
id: z.string(),
|
||||
expirationTime: z.string(),
|
||||
macSecretBase64: z.string(),
|
||||
});
|
||||
|
||||
type WebhookRegistrationData = z.infer<typeof WebhookRegistrationDataSchema>;
|
||||
|
||||
const WebhookListDataSchema = z.object({
|
||||
webhooks: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
notificationUrl: z.string(),
|
||||
expirationTime: z.coerce.date(),
|
||||
areNotificationsEnabled: z.boolean(),
|
||||
isHookEnabled: z.boolean(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
type WebhookListData = z.infer<typeof WebhookListDataSchema>;
|
||||
|
||||
export function createWebhookEventSource(
|
||||
integration: Airtable
|
||||
): ExternalSource<
|
||||
Airtable,
|
||||
{ baseId: string; tableId?: string },
|
||||
"HTTP",
|
||||
{ dataTypes: WebhookDataType[]; fromSources?: WebhookFromSource[] }
|
||||
> {
|
||||
return new ExternalSource("HTTP", {
|
||||
id: "airtable.webhook",
|
||||
schema: z.object({ baseId: z.string(), tableId: z.string().optional() }),
|
||||
optionSchema: z.object({
|
||||
dataTypes: z.array(WebhookDataTypeSchema),
|
||||
fromSources: z.array(WebhookFromSourceSchema).optional(),
|
||||
}),
|
||||
version: "0.1.0",
|
||||
integration,
|
||||
filter: (params, options) => ({
|
||||
actionMetadata: {
|
||||
source: options?.fromSources ?? ["client", "anonymousUser", "formSubmission"],
|
||||
},
|
||||
}),
|
||||
key: (params) =>
|
||||
`airtable.webhook.${params.baseId}${params.tableId ? `.${params.tableId}` : ""}`,
|
||||
handler: webhookHandler,
|
||||
register: async (event, io, ctx) => {
|
||||
const { params, source: httpSource, options } = event;
|
||||
|
||||
const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data);
|
||||
|
||||
const registeredOptions = {
|
||||
event: options.event.desired,
|
||||
dataTypes: options.dataTypes.desired,
|
||||
fromSources: options.fromSources?.desired,
|
||||
};
|
||||
|
||||
const specification: WebhookSpecification = {
|
||||
filters: {
|
||||
dataTypes: options.dataTypes.desired as WebhookDataType[],
|
||||
changeTypes: options.event.desired as WebhookChangeType[],
|
||||
fromSources: (options.fromSources?.desired ?? [
|
||||
"client",
|
||||
"anonymousUser",
|
||||
"formSubmission",
|
||||
]) as WebhookFromSource[],
|
||||
recordChangeScope: params.tableId,
|
||||
},
|
||||
};
|
||||
|
||||
if (httpSource.active && webhookData.success) {
|
||||
const hasMissingOptions = Object.values(options).some(
|
||||
(option) => option.missing.length > 0
|
||||
);
|
||||
if (!hasMissingOptions) return;
|
||||
|
||||
const updatedWebhook = await io.integration.webhooks().update("update-webhook", {
|
||||
baseId: params.baseId,
|
||||
url: httpSource.url,
|
||||
webhookId: webhookData.data.id,
|
||||
options: specification,
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const listResponse = await io.integration.webhooks().list("list-webhooks", {
|
||||
baseId: params.baseId,
|
||||
});
|
||||
|
||||
const existingWebhook = listResponse.webhooks.find(
|
||||
(w) => w.notificationUrl === httpSource.url
|
||||
);
|
||||
|
||||
if (existingWebhook) {
|
||||
const updatedWebhook = await io.integration.webhooks().update("update-webhook", {
|
||||
baseId: params.baseId,
|
||||
url: httpSource.url,
|
||||
webhookId: existingWebhook.id,
|
||||
options: specification,
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const webhook = await io.integration.webhooks().create("create-webhook", {
|
||||
url: httpSource.url,
|
||||
baseId: params.baseId,
|
||||
options: specification,
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(webhook),
|
||||
secret: webhook.macSecretBase64,
|
||||
options: registeredOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** This is the data received from Airtable. It's not useful on its own */
|
||||
const ReceivedPayload = z.object({
|
||||
base: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
webhook: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
timestamp: z.coerce.date(),
|
||||
});
|
||||
|
||||
const SourceMetadataSchema = z
|
||||
.object({
|
||||
cursor: z.number().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: Airtable) {
|
||||
logger.debug("[@trigger.dev/airtable] Handling webhook payload");
|
||||
|
||||
const client = integration.createClient(event.source.auth);
|
||||
|
||||
const { rawEvent: request, source } = event;
|
||||
|
||||
if (!request.body) {
|
||||
logger.debug("[@trigger.dev/airtable] No body found");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
const rawBody = await request.text();
|
||||
|
||||
const signature = request.headers.get("X-Airtable-Content-MAC");
|
||||
|
||||
if (!signature) {
|
||||
logger.error("[@trigger.dev/airtable] Error validating webhook signature, no signature found");
|
||||
throw Error("[@trigger.dev/airtable] No signature found");
|
||||
}
|
||||
|
||||
const hmac = require("crypto").createHmac("sha256", source.secret);
|
||||
hmac.update(rawBody, "ascii");
|
||||
const expectedContentHmac = "hmac-sha256=" + hmac.digest("hex");
|
||||
|
||||
if (signature !== expectedContentHmac) {
|
||||
logger.error("[@trigger.dev/airtable] Error validating webhook signature, they don't match");
|
||||
}
|
||||
|
||||
const webhookPayload = ReceivedPayload.parse(JSON.parse(rawBody));
|
||||
const parsedMetadata = SourceMetadataSchema.parse(source.metadata);
|
||||
|
||||
//fetch the actual payloads
|
||||
const response = await getAllPayloads(
|
||||
webhookPayload.base.id,
|
||||
webhookPayload.webhook.id,
|
||||
client,
|
||||
parsedMetadata?.cursor
|
||||
);
|
||||
|
||||
return {
|
||||
events: response
|
||||
? response.payloads.map((payload) => ({
|
||||
id: `${payload.timestamp}-${payload.baseTransactionNumber}`,
|
||||
payload: payload,
|
||||
source: "airtable.com",
|
||||
name: "changed",
|
||||
timestamp: payload.timestamp,
|
||||
context: {},
|
||||
}))
|
||||
: [],
|
||||
metadata: response?.cursor ? { cursor: response.cursor } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllPayloads(
|
||||
baseId: string,
|
||||
webhookId: string,
|
||||
sdk: AirtableSDK,
|
||||
cursor: number | undefined
|
||||
) {
|
||||
let response: ListWebhooksResponse | undefined = undefined;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const newResponse = await getPayload(baseId, webhookId, sdk, cursor);
|
||||
cursor = newResponse.cursor;
|
||||
hasMore = newResponse.mightHaveMore;
|
||||
|
||||
if (response) {
|
||||
response.payloads.push(...newResponse.payloads);
|
||||
} else {
|
||||
response = newResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function getPayload(
|
||||
baseId: string,
|
||||
webhookId: string,
|
||||
sdk: AirtableSDK,
|
||||
cursor: number | undefined
|
||||
) {
|
||||
const url = new URL(`${apiUrl}/${baseId}/webhooks/${webhookId}/payloads`);
|
||||
if (cursor) {
|
||||
url.searchParams.append("cursor", cursor.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(url.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${sdk._apiKey}`,
|
||||
},
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list webhooks: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const webhook = await response.json();
|
||||
return ListWebhooksResponseSchema.parse(webhook);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@trigger.dev/tsconfig/node18.json",
|
||||
"include": ["./src/**/*.ts", "tsup.config.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"paths": {
|
||||
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
|
||||
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
|
||||
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
|
||||
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"]
|
||||
},
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: "main",
|
||||
entry: ["./src/index.ts"],
|
||||
outDir: "./dist",
|
||||
platform: "node",
|
||||
format: ["cjs"],
|
||||
legacyOutput: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
dts: true,
|
||||
treeshake: {
|
||||
preset: "smallest",
|
||||
},
|
||||
esbuildPlugins: [],
|
||||
external: ["http", "https", "util", "events", "tty", "os", "timers"],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0`
|
||||
- `@trigger.dev/sdk@2.1.0`
|
||||
|
||||
## 2.1.0-beta.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.1`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.1`
|
||||
|
||||
## 2.1.0-beta.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.0`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.0`
|
||||
|
||||
## 2.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "2.0.14",
|
||||
"version": "2.1.0",
|
||||
"description": "The official GitHub integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -29,8 +29,8 @@
|
||||
"@octokit/request": "^6.2.5",
|
||||
"@octokit/request-error": "^4.0.1",
|
||||
"@octokit/webhooks": "^10.4.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.0.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.0.14",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.0",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.0",
|
||||
"octokit": "^2.0.14",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ClientFactory } from "@trigger.dev/sdk";
|
||||
import { Octokit } from "octokit";
|
||||
|
||||
export const clientFactory: ClientFactory<Octokit> = (auth) => {
|
||||
return new Octokit({
|
||||
auth: auth.accessToken,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Octokit } from "octokit";
|
||||
import { GitHubReturnType, GitHubRunTask, onError } from "./index";
|
||||
import { Issues } from "./issues";
|
||||
import { ReactionContent, Reactions } from "./reactions";
|
||||
|
||||
export class Compound {
|
||||
runTask: GitHubRunTask;
|
||||
issues: Issues;
|
||||
reactions: Reactions;
|
||||
|
||||
constructor(runTask: GitHubRunTask, issues: Issues, reactions: Reactions) {
|
||||
this.runTask = runTask;
|
||||
this.issues = issues;
|
||||
this.reactions = reactions;
|
||||
}
|
||||
|
||||
createIssueCommentWithReaction(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
body: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
issueNumber: number;
|
||||
reaction: ReactionContent;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["issues"]["createComment"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async () => {
|
||||
const comment = await this.issues.createComment(
|
||||
`Comment on Issue #${params.issueNumber}`,
|
||||
params
|
||||
);
|
||||
const reaction = await this.reactions.createForIssueComment(
|
||||
`React with ${params.reaction}`,
|
||||
{
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
commentId: comment.id,
|
||||
content: params.reaction,
|
||||
}
|
||||
);
|
||||
return comment;
|
||||
},
|
||||
{
|
||||
name: "Create Issue Comment",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
{
|
||||
label: "Issue",
|
||||
text: `#${params.issueNumber}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Octokit } from "octokit";
|
||||
import { GitHubReturnType, GitHubRunTask, onError } from "./index";
|
||||
import { repoProperties } from "./propertyHelpers";
|
||||
|
||||
type Endcoding = "utf-8" | "base-64";
|
||||
|
||||
type AuthorContent = {
|
||||
name: string;
|
||||
email: string;
|
||||
date?: string;
|
||||
};
|
||||
|
||||
type CommitterContent = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
date?: string;
|
||||
};
|
||||
|
||||
type TagType = "commit" | "tree" | "blob";
|
||||
|
||||
type TaggerContent = {
|
||||
name: string;
|
||||
email: string;
|
||||
date?: string;
|
||||
};
|
||||
|
||||
type TreeType = {
|
||||
path?: string | undefined;
|
||||
mode?: "100644" | "100755" | "040000" | "160000" | "120000" | undefined;
|
||||
type?: "commit" | "tree" | "blob" | undefined;
|
||||
sha?: string | null | undefined;
|
||||
content?: string | undefined;
|
||||
};
|
||||
|
||||
export class Git {
|
||||
runTask: GitHubRunTask;
|
||||
|
||||
constructor(runTask: GitHubRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
createBlob(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
content: string;
|
||||
encoding?: Endcoding;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["createBlob"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.createBlob({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
content: params.content,
|
||||
encoding: params.encoding,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Blob",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Content",
|
||||
text: params.content,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
getBlob(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
fileSHA: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["getBlob"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.getBlob({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
file_sha: params.fileSHA,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Blob",
|
||||
params,
|
||||
properties: [...repoProperties(params)],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createCommit(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
message: string;
|
||||
tree: string;
|
||||
parents?: string[];
|
||||
author?: AuthorContent;
|
||||
committer?: CommitterContent;
|
||||
signature?: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["createCommit"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.createCommit({
|
||||
...params,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Commit",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Message",
|
||||
text: params.message,
|
||||
},
|
||||
{
|
||||
label: "Tree",
|
||||
text: params.tree,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
getCommit(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
commitSHA: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["getCommit"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.getCommit({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
commit_sha: params.commitSHA,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Commit",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Commit SHA",
|
||||
text: params.commitSHA,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
listMatchingRefs(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["listMatchingRefs"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.listMatchingRefs(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "List Matching References",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Ref",
|
||||
text: params.ref,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
getRef(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["getRef"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.getRef(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Reference",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Ref",
|
||||
text: params.ref,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createRef(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
sha: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["createRef"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.createRef(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Reference",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Ref",
|
||||
text: params.ref,
|
||||
},
|
||||
{
|
||||
label: "SHA",
|
||||
text: params.ref,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
updateRef(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
sha: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["updateRef"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.updateRef(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Update Reference",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Ref",
|
||||
text: params.ref,
|
||||
},
|
||||
{
|
||||
label: "SHA",
|
||||
text: params.ref,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
deleteRef(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["deleteRef"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.deleteRef(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Delete Reference",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Ref",
|
||||
text: params.ref,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createTag(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag: string;
|
||||
message: string;
|
||||
object: string;
|
||||
type: TagType;
|
||||
tagger?: TaggerContent;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["createTag"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.createTag(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Tag",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Tag",
|
||||
text: params.tag,
|
||||
},
|
||||
{
|
||||
label: "Message",
|
||||
text: params.message,
|
||||
},
|
||||
{
|
||||
label: "Object",
|
||||
text: params.object,
|
||||
},
|
||||
{
|
||||
label: "Tag Type",
|
||||
text: params.type,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
getTag(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tagSHA: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["getTag"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.getTag({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
tag_sha: params.tagSHA,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Tag",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Tag SHA",
|
||||
text: params.tagSHA,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createTree(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tree: TreeType[];
|
||||
baseTree?: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["createTree"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.createTree(params);
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Tree",
|
||||
params,
|
||||
properties: [...repoProperties(params)],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
getTree(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
treeSHA: string;
|
||||
recursive?: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["git"]["getTree"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.git.getTree({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
tree_sha: params.treeSHA,
|
||||
recursive: params.recursive,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Tree",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Tree SHA",
|
||||
text: params.treeSHA,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RequestError } from "@octokit/request-error";
|
||||
import { RequestRequestOptions } from "@octokit/types";
|
||||
import {
|
||||
CreateEvent,
|
||||
@@ -12,15 +13,22 @@ import {
|
||||
StarCreatedEvent,
|
||||
StarEvent,
|
||||
} from "@octokit/webhooks-types";
|
||||
import { truncate } from "@trigger.dev/integration-kit";
|
||||
import {
|
||||
ConnectionAuth,
|
||||
EventSpecification,
|
||||
ExternalSourceTrigger,
|
||||
IntegrationClient,
|
||||
IO,
|
||||
IOTask,
|
||||
IntegrationTaskKey,
|
||||
Json,
|
||||
RunTaskErrorCallback,
|
||||
RunTaskOptions,
|
||||
TriggerIntegration,
|
||||
retry,
|
||||
} from "@trigger.dev/sdk";
|
||||
import { Octokit } from "octokit";
|
||||
import { createOrgEventSource, createRepoEventSource } from "./sources";
|
||||
import { tasks } from "./tasks";
|
||||
import {
|
||||
issueAssigned,
|
||||
issueCommentCreated,
|
||||
@@ -31,7 +39,12 @@ import {
|
||||
push,
|
||||
starredRepo,
|
||||
} from "./webhook-examples";
|
||||
import { truncate } from "@trigger.dev/integration-kit";
|
||||
import { Issues } from "./issues";
|
||||
import { Repos } from "./repos";
|
||||
import { Reactions } from "./reactions";
|
||||
import { Compound } from "./compound";
|
||||
import { Orgs } from "./orgs";
|
||||
import { Git } from "./git";
|
||||
|
||||
export type GithubIntegrationOptions = {
|
||||
id: string;
|
||||
@@ -49,15 +62,29 @@ type GithubTriggers = {
|
||||
org: ReturnType<typeof createOrgTrigger>;
|
||||
};
|
||||
|
||||
export class Github implements TriggerIntegration<IntegrationClient<Octokit, typeof tasks>> {
|
||||
client: IntegrationClient<Octokit, typeof tasks>;
|
||||
export type GitHubRunTask = InstanceType<typeof Github>["runTask"];
|
||||
export type GitHubReturnType<T extends (params: any) => Promise<{ data: K }>, K = any> = Promise<
|
||||
Awaited<ReturnType<T>>["data"]
|
||||
>;
|
||||
|
||||
export class Github implements TriggerIntegration {
|
||||
private _options: GithubIntegrationOptions;
|
||||
private _client?: Octokit;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
_repoSource: ReturnType<typeof createRepoEventSource>;
|
||||
_orgSource: ReturnType<typeof createOrgEventSource>;
|
||||
_repoTrigger: ReturnType<typeof createRepoTrigger>;
|
||||
_orgTrigger: ReturnType<typeof createOrgTrigger>;
|
||||
|
||||
constructor(private options: GithubIntegrationOptions) {
|
||||
this.client = createClientFromOptions(options);
|
||||
if (Object.keys(options).includes("token") && !options.token) {
|
||||
throw `Can't create GitHub integration (${options.id}) as token was undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
|
||||
this._repoSource = createRepoEventSource(this);
|
||||
this._orgSource = createOrgEventSource(this);
|
||||
this._repoTrigger = createRepoTrigger(this._repoSource);
|
||||
@@ -72,6 +99,18 @@ export class Github implements TriggerIntegration<IntegrationClient<Octokit, typ
|
||||
return { name: "GitHub", id: "github" };
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
return this._options.token ? ("LOCAL" as const) : ("HOSTED" as const);
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const github = new Github(this._options);
|
||||
github._io = io;
|
||||
github._connectionKey = connectionKey;
|
||||
github._client = createClientFromOptions(this._options, auth);
|
||||
return github;
|
||||
}
|
||||
|
||||
get sources(): GithubSources {
|
||||
return {
|
||||
repo: this._repoSource,
|
||||
@@ -85,45 +124,114 @@ export class Github implements TriggerIntegration<IntegrationClient<Octokit, typ
|
||||
org: this._orgTrigger,
|
||||
};
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: Octokit, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "github",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
|
||||
get issues() {
|
||||
return new Issues(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get repos() {
|
||||
return new Repos(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get reactions() {
|
||||
return new Reactions(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get compound() {
|
||||
return new Compound(this.runTask.bind(this), this.issues, this.reactions);
|
||||
}
|
||||
|
||||
get orgs() {
|
||||
return new Orgs(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get git() {
|
||||
return new Git(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
createIssue = this.issues.create;
|
||||
addIssueAssignees = this.issues.addAssignees;
|
||||
addIssueLabels = this.issues.addLabels;
|
||||
createIssueComment = this.issues.createComment;
|
||||
getIssue = this.issues.get;
|
||||
getRepo = this.repos.get;
|
||||
updateWebhook = this.repos.updateWebhook;
|
||||
createWebhook = this.repos.createWebhook;
|
||||
listWebhooks = this.repos.listWebhooks;
|
||||
addIssueCommentReaction = this.reactions.createForIssueComment;
|
||||
createIssueCommentWithReaction = this.compound.createIssueCommentWithReaction;
|
||||
updateOrgWebhook = this.orgs.updateWebhook;
|
||||
createOrgWebhook = this.orgs.createWebhook;
|
||||
listOrgWebhooks = this.orgs.listWebhooks;
|
||||
createBlob = this.git.createBlob;
|
||||
getBlob = this.git.getBlob;
|
||||
createCommit = this.git.createCommit;
|
||||
getCommit = this.git.getCommit;
|
||||
listMatchingReferences = this.git.listMatchingRefs;
|
||||
getReference = this.git.getRef;
|
||||
createReference = this.git.createRef;
|
||||
updateReference = this.git.updateRef;
|
||||
deleteReference = this.git.deleteRef;
|
||||
createTag = this.git.createTag;
|
||||
getTag = this.git.getTag;
|
||||
createTree = this.git.createTree;
|
||||
getTree = this.git.getTree;
|
||||
}
|
||||
|
||||
function createClientFromOptions(
|
||||
options: GithubIntegrationOptions
|
||||
): IntegrationClient<Octokit, typeof tasks> {
|
||||
options: GithubIntegrationOptions,
|
||||
auth?: ConnectionAuth
|
||||
): Octokit {
|
||||
if (Object.keys(options).includes("token") && !options.token) {
|
||||
throw `Can't create GitHub integration (${options.id}) as token was undefined`;
|
||||
}
|
||||
|
||||
if (options.token) {
|
||||
const client = new Octokit({
|
||||
return new Octokit({
|
||||
auth: options.token,
|
||||
request: options.octokitRequest,
|
||||
retry: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
usesLocalAuth: true,
|
||||
client,
|
||||
tasks,
|
||||
auth: options.token,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
usesLocalAuth: false,
|
||||
clientFactory: (auth) => {
|
||||
return new Octokit({
|
||||
auth: auth.accessToken,
|
||||
request: options.octokitRequest,
|
||||
retry: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
if (!auth) {
|
||||
throw new Error("No auth");
|
||||
}
|
||||
|
||||
return new Octokit({
|
||||
auth: auth.accessToken,
|
||||
request: options.octokitRequest,
|
||||
retry: {
|
||||
enabled: false,
|
||||
},
|
||||
tasks,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const onIssue: EventSpecification<IssuesEvent> = {
|
||||
@@ -461,6 +569,7 @@ function createRepoTrigger(
|
||||
event,
|
||||
params: { owner, repo },
|
||||
source,
|
||||
options: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -484,6 +593,32 @@ function createOrgTrigger(
|
||||
event,
|
||||
params: { org },
|
||||
source,
|
||||
options: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function isRequestError(error: unknown): error is RequestError {
|
||||
return typeof error === "object" && error !== null && "status" in error;
|
||||
}
|
||||
|
||||
export function onError(error: unknown) {
|
||||
if (!isRequestError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a rate limit error
|
||||
if (error.status === 403 && error.response) {
|
||||
const rateLimitRemaining = error.response.headers["x-ratelimit-remaining"];
|
||||
const rateLimitReset = error.response.headers["x-ratelimit-reset"];
|
||||
|
||||
if (rateLimitRemaining === "0" && rateLimitReset) {
|
||||
const resetDate = new Date(Number(rateLimitReset) * 1000);
|
||||
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { truncate } from "@trigger.dev/integration-kit";
|
||||
import { IntegrationTaskKey, Prettify, retry } from "@trigger.dev/sdk";
|
||||
import { GitHubReturnType, GitHubRunTask, onError } from "./index";
|
||||
import { Octokit } from "octokit";
|
||||
import { issueProperties, repoProperties } from "./propertyHelpers";
|
||||
|
||||
type AddIssueLabels = GitHubReturnType<Octokit["rest"]["issues"]["addLabels"]>;
|
||||
export class Issues {
|
||||
runTask: GitHubRunTask;
|
||||
|
||||
constructor(runTask: GitHubRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: { title: string; owner: string; repo: string }
|
||||
): GitHubReturnType<Octokit["rest"]["issues"]["create"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.issues.create({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
title: params.title,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Issue",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
{
|
||||
label: "Title",
|
||||
text: params.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
addAssignees(
|
||||
key: IntegrationTaskKey,
|
||||
params: { owner: string; repo: string; issueNumber: number; assignees: string[] }
|
||||
): GitHubReturnType<Octokit["rest"]["issues"]["addAssignees"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.issues.addAssignees({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
issue_number: params.issueNumber,
|
||||
assignees: params.assignees,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Add Issue Assignees",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
...issueProperties(params),
|
||||
{
|
||||
label: "assignees",
|
||||
text: params.assignees.join(", "),
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
addLabels(
|
||||
key: IntegrationTaskKey,
|
||||
params: { owner: string; repo: string; issueNumber: number; labels: string[] }
|
||||
): AddIssueLabels {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.issues.addLabels({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
issue_number: params.issueNumber,
|
||||
labels: params.labels,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Add Issue Labels",
|
||||
params,
|
||||
properties: [
|
||||
...repoProperties(params),
|
||||
...issueProperties(params),
|
||||
{
|
||||
label: "Labels",
|
||||
text: params.labels.join(", "),
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createComment(
|
||||
key: IntegrationTaskKey,
|
||||
params: { body: string; owner: string; repo: string; issueNumber: number }
|
||||
): GitHubReturnType<Octokit["rest"]["issues"]["createComment"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.issues.createComment({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
body: params.body,
|
||||
issue_number: params.issueNumber,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Issue Comment",
|
||||
params,
|
||||
properties: [...repoProperties(params), ...issueProperties(params)],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: { owner: string; repo: string; issueNumber: number }
|
||||
): GitHubReturnType<Octokit["rest"]["issues"]["get"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.issues.get({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
issue_number: params.issueNumber,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Issue",
|
||||
params,
|
||||
properties: [...repoProperties(params), ...issueProperties(params)],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Octokit } from "octokit";
|
||||
import { GitHubReturnType, GitHubRunTask, onError } from "./index";
|
||||
|
||||
export class Orgs {
|
||||
runTask: GitHubRunTask;
|
||||
|
||||
constructor(runTask: GitHubRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
updateWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
org: string;
|
||||
hookId: number;
|
||||
url: string;
|
||||
secret: string;
|
||||
addEvents?: string[];
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["orgs"]["updateWebhook"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.orgs.updateWebhook({
|
||||
org: params.org,
|
||||
hook_id: params.hookId,
|
||||
config: {
|
||||
content_type: "json",
|
||||
url: params.url,
|
||||
secret: params.secret,
|
||||
},
|
||||
add_events: params.addEvents,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Update Org Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Org",
|
||||
text: params.org,
|
||||
},
|
||||
{
|
||||
label: "Hook ID",
|
||||
text: String(params.hookId),
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
org: string;
|
||||
url: string;
|
||||
secret: string;
|
||||
events: string[];
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["orgs"]["createWebhook"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.orgs.createWebhook({
|
||||
org: params.org,
|
||||
name: "web",
|
||||
config: {
|
||||
content_type: "json",
|
||||
url: params.url,
|
||||
secret: params.secret,
|
||||
},
|
||||
events: params.events,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Org Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Org",
|
||||
text: params.org,
|
||||
},
|
||||
{
|
||||
label: "Events",
|
||||
text: params.events.join(", "),
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
listWebhooks(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
org: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["orgs"]["listWebhooks"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.orgs.listWebhooks({
|
||||
org: params.org,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "List Org Webhooks",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Org",
|
||||
text: params.org,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { truncate } from "@trigger.dev/integration-kit";
|
||||
import { IntegrationTaskKey, Prettify, retry } from "@trigger.dev/sdk";
|
||||
import { GitHubReturnType, GitHubRunTask, onError } from "./index";
|
||||
import { Octokit } from "octokit";
|
||||
import { issueProperties, repoProperties } from "./propertyHelpers";
|
||||
|
||||
export type ReactionContent =
|
||||
| "+1"
|
||||
| "-1"
|
||||
| "laugh"
|
||||
| "confused"
|
||||
| "heart"
|
||||
| "hooray"
|
||||
| "rocket"
|
||||
| "eyes";
|
||||
|
||||
export class Reactions {
|
||||
runTask: GitHubRunTask;
|
||||
|
||||
constructor(runTask: GitHubRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
createForIssueComment(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
commentId: number;
|
||||
content: ReactionContent;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["reactions"]["createForIssueComment"]> {
|
||||
let emoji = "";
|
||||
|
||||
switch (params.content) {
|
||||
case "+1":
|
||||
emoji = "👍";
|
||||
break;
|
||||
case "-1":
|
||||
emoji = "👎";
|
||||
break;
|
||||
case "laugh":
|
||||
emoji = "😄";
|
||||
break;
|
||||
case "confused":
|
||||
emoji = "😕";
|
||||
break;
|
||||
case "heart":
|
||||
emoji = "❤️";
|
||||
break;
|
||||
case "hooray":
|
||||
emoji = "🎉";
|
||||
break;
|
||||
case "rocket":
|
||||
emoji = "🚀";
|
||||
break;
|
||||
case "eyes":
|
||||
emoji = "👀";
|
||||
break;
|
||||
}
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.reactions.createForIssueComment({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
comment_id: params.commentId,
|
||||
content: params.content,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Add Issue Reaction",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
{
|
||||
label: "Comment",
|
||||
text: `#${params.commentId}`,
|
||||
},
|
||||
{ label: "reaction", text: emoji },
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { truncate } from "@trigger.dev/integration-kit";
|
||||
import { IntegrationTaskKey, Prettify, retry } from "@trigger.dev/sdk";
|
||||
import { GitHubReturnType, GitHubRunTask, onError } from "./index";
|
||||
import { Octokit } from "octokit";
|
||||
import { issueProperties, repoProperties } from "./propertyHelpers";
|
||||
|
||||
export class Repos {
|
||||
runTask: GitHubRunTask;
|
||||
|
||||
constructor(runTask: GitHubRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: { owner: string; repo: string }
|
||||
): GitHubReturnType<Octokit["rest"]["repos"]["get"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.repos.get({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
headers: {
|
||||
"x-trigger-attempt": String(task.attempts),
|
||||
},
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Get Repo",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
updateWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
hookId: number;
|
||||
url: string;
|
||||
secret: string;
|
||||
addEvents?: string[];
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["repos"]["updateWebhook"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.repos.updateWebhook({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
hook_id: params.hookId,
|
||||
config: {
|
||||
content_type: "json",
|
||||
url: params.url,
|
||||
secret: params.secret,
|
||||
},
|
||||
add_events: params.addEvents,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Update Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Owner",
|
||||
text: params.owner,
|
||||
},
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
{
|
||||
label: "Hook ID",
|
||||
text: String(params.hookId),
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
createWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
url: string;
|
||||
secret: string;
|
||||
events: string[];
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["repos"]["createWebhook"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.repos.createWebhook({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
config: {
|
||||
content_type: "json",
|
||||
url: params.url,
|
||||
secret: params.secret,
|
||||
},
|
||||
events: params.events,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "Create Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Owner",
|
||||
text: params.owner,
|
||||
},
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
{
|
||||
label: "Events",
|
||||
text: params.events.join(", "),
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
listWebhooks(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
}
|
||||
): GitHubReturnType<Octokit["rest"]["repos"]["listWebhooks"]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const result = await client.rest.repos.listWebhooks({
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
});
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "List Webhooks",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Owner",
|
||||
text: params.owner,
|
||||
},
|
||||
{
|
||||
label: "Repo",
|
||||
text: params.repo,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Webhooks } from "@octokit/webhooks";
|
||||
import {
|
||||
IntegrationClient,
|
||||
ExternalSource,
|
||||
TriggerIntegration,
|
||||
HandlerEvent,
|
||||
} from "@trigger.dev/sdk";
|
||||
import { ExternalSource, TriggerIntegration, HandlerEvent } from "@trigger.dev/sdk";
|
||||
import type { Logger } from "@trigger.dev/sdk";
|
||||
import { safeJsonParse, omit } from "@trigger.dev/integration-kit";
|
||||
import { Octokit } from "octokit";
|
||||
import { z } from "zod";
|
||||
import { tasks } from "./tasks";
|
||||
import { Github } from "./index";
|
||||
|
||||
type WebhookData = {
|
||||
id: number;
|
||||
@@ -30,12 +25,8 @@ function webhookData(data: any): data is WebhookData {
|
||||
}
|
||||
|
||||
export function createRepoEventSource(
|
||||
integration: TriggerIntegration<IntegrationClient<Octokit, typeof tasks>>
|
||||
): ExternalSource<
|
||||
TriggerIntegration<IntegrationClient<Octokit, typeof tasks>>,
|
||||
{ owner: string; repo: string },
|
||||
"HTTP"
|
||||
> {
|
||||
integration: Github
|
||||
): ExternalSource<Github, { owner: string; repo: string }, "HTTP", {}> {
|
||||
return new ExternalSource("HTTP", {
|
||||
id: "github.repo",
|
||||
version: "0.1.1",
|
||||
@@ -61,27 +52,32 @@ export function createRepoEventSource(
|
||||
}),
|
||||
handler: webhookHandler,
|
||||
register: async (event, io, ctx) => {
|
||||
const { params, source: httpSource, events, missingEvents } = event;
|
||||
const { params, source: httpSource, options } = event;
|
||||
|
||||
const registeredOptions = {
|
||||
event: options.event.desired,
|
||||
};
|
||||
|
||||
if (httpSource.active && webhookData(httpSource.data)) {
|
||||
if (missingEvents.length > 0) {
|
||||
// We need to update the webhook to add the new events and then return
|
||||
const newWebhookData = await io.integration.updateWebhook("update-webhook", {
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
hookId: httpSource.data.id,
|
||||
url: httpSource.url,
|
||||
secret: httpSource.secret,
|
||||
addEvents: missingEvents,
|
||||
});
|
||||
const hasMissingOptions = Object.values(options).some(
|
||||
(option) => option.missing.length > 0
|
||||
);
|
||||
if (!hasMissingOptions) return;
|
||||
|
||||
return {
|
||||
data: newWebhookData,
|
||||
registeredEvents: newWebhookData.events,
|
||||
};
|
||||
}
|
||||
// We need to update the webhook to add the new events and then return
|
||||
const newWebhookData = await io.integration.updateWebhook("update-webhook", {
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
hookId: httpSource.data.id,
|
||||
url: httpSource.url,
|
||||
secret: httpSource.secret,
|
||||
addEvents: options.event.missing,
|
||||
});
|
||||
|
||||
return;
|
||||
return {
|
||||
data: newWebhookData,
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const webhooks = await io.integration.listWebhooks("list-webhooks", {
|
||||
@@ -99,35 +95,31 @@ export function createRepoEventSource(
|
||||
hookId: existingWebhook.id,
|
||||
url: httpSource.url,
|
||||
secret: httpSource.secret,
|
||||
addEvents: missingEvents,
|
||||
addEvents: options.event.missing,
|
||||
});
|
||||
|
||||
return {
|
||||
data: updatedWebhook,
|
||||
registeredEvents: updatedWebhook.events,
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const webhook = await io.integration.createWebhook("create-webhook", {
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
events,
|
||||
events: options.event.desired,
|
||||
url: httpSource.url,
|
||||
secret: httpSource.secret,
|
||||
});
|
||||
|
||||
return { data: webhook, registeredEvents: webhook.events };
|
||||
return { data: webhook, options: registeredOptions };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createOrgEventSource(
|
||||
integration: TriggerIntegration<IntegrationClient<Octokit, typeof tasks>>
|
||||
): ExternalSource<
|
||||
TriggerIntegration<IntegrationClient<Octokit, typeof tasks>>,
|
||||
{ org: string },
|
||||
"HTTP"
|
||||
> {
|
||||
integration: Github
|
||||
): ExternalSource<Github, { org: string }, "HTTP", {}> {
|
||||
return new ExternalSource("HTTP", {
|
||||
id: "github.org",
|
||||
version: "0.1.1",
|
||||
@@ -148,13 +140,19 @@ export function createOrgEventSource(
|
||||
}),
|
||||
handler: webhookHandler,
|
||||
register: async (event, io, ctx) => {
|
||||
const { params, source: httpSource, events, missingEvents } = event;
|
||||
const { params, source: httpSource, options } = event;
|
||||
|
||||
const registeredOptions = {
|
||||
event: options.event.desired,
|
||||
};
|
||||
|
||||
const hasMissingOptions = Object.values(options).some((option) => option.missing.length > 0);
|
||||
|
||||
if (
|
||||
httpSource.active &&
|
||||
webhookData(httpSource.data) &&
|
||||
httpSource.secret &&
|
||||
missingEvents.length > 0
|
||||
hasMissingOptions
|
||||
) {
|
||||
const existingData = httpSource.data;
|
||||
|
||||
@@ -164,13 +162,13 @@ export function createOrgEventSource(
|
||||
hookId: existingData.id,
|
||||
url: httpSource.url,
|
||||
secret: httpSource.secret,
|
||||
addEvents: missingEvents,
|
||||
addEvents: options.event.missing,
|
||||
});
|
||||
|
||||
return {
|
||||
secret: httpSource.secret,
|
||||
data: newWebhookData,
|
||||
registeredEvents: newWebhookData.events,
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,18 +191,18 @@ export function createOrgEventSource(
|
||||
return {
|
||||
secret,
|
||||
data: updatedWebhook,
|
||||
registeredEvents: updatedWebhook.events,
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const webhook = await io.integration.createOrgWebhook("create-webhook", {
|
||||
org: params.org,
|
||||
events,
|
||||
events: options.event.desired,
|
||||
url: httpSource.url,
|
||||
secret,
|
||||
});
|
||||
|
||||
return { secret, data: webhook, registeredEvents: webhook.events };
|
||||
return { secret, data: webhook, options: registeredOptions };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,37 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0`
|
||||
- `@trigger.dev/sdk@2.1.0`
|
||||
|
||||
## 2.1.0-beta.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.1`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.1`
|
||||
|
||||
## 2.1.0-beta.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.0`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.0`
|
||||
|
||||
## 2.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/openai",
|
||||
"version": "2.0.14",
|
||||
"version": "2.1.0",
|
||||
"description": "The official OpenAI integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,8 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"openai": "^4.2.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.0.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.0.14"
|
||||
"@trigger.dev/sdk": "workspace:^2.1.0",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { IntegrationTaskKey, Prettify, redactString } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
import { createTaskUsageProperties } from "./taskUtils";
|
||||
|
||||
export class Chat {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
completions = {
|
||||
create: (
|
||||
key: IntegrationTaskKey,
|
||||
params: Prettify<OpenAI.Chat.CompletionCreateParamsNonStreaming>
|
||||
): Promise<OpenAI.Chat.ChatCompletion> => {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.chat.completions.create(params);
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Chat Completion",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
backgroundCreate: (
|
||||
key: IntegrationTaskKey,
|
||||
params: Prettify<OpenAI.Chat.CompletionCreateParamsNonStreaming>
|
||||
): Promise<OpenAI.Chat.ChatCompletion> => {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
const response = await io.backgroundFetch<OpenAI.Chat.ChatCompletion>(
|
||||
"background",
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: redactString`Bearer ${client.apiKey}`,
|
||||
...(client.organization ? { "OpenAI-Organization": client.organization } : {}),
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
},
|
||||
{
|
||||
"500-599": {
|
||||
strategy: "backoff",
|
||||
limit: 5,
|
||||
minTimeoutInMs: 1000,
|
||||
maxTimeoutInMs: 30000,
|
||||
factor: 1.8,
|
||||
randomize: true,
|
||||
},
|
||||
"429": {
|
||||
strategy: "backoff",
|
||||
limit: 10,
|
||||
minTimeoutInMs: 1000,
|
||||
maxTimeoutInMs: 60000,
|
||||
factor: 2,
|
||||
randomize: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Background Chat Completion",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { IntegrationTaskKey, Prettify, redactString } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
import { createTaskUsageProperties } from "./taskUtils";
|
||||
|
||||
export class Completions {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: Prettify<OpenAI.CompletionCreateParamsNonStreaming>
|
||||
): Promise<OpenAI.Completion> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.completions.create(params);
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Completion",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
backgroundCreate(
|
||||
key: IntegrationTaskKey,
|
||||
params: Prettify<OpenAI.CompletionCreateParamsNonStreaming>
|
||||
): Promise<OpenAI.Completion> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
const response = await io.backgroundFetch<OpenAI.Completion>(
|
||||
"background",
|
||||
"https://api.openai.com/v1/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: redactString`Bearer ${client.apiKey}`,
|
||||
...(client.organization ? { "OpenAI-Organization": client.organization } : {}),
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
}
|
||||
);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Background Completion",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { truncate } from "@trigger.dev/integration-kit";
|
||||
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
import { createTaskUsageProperties } from "./taskUtils";
|
||||
|
||||
export class Edits {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated The Edits API is deprecated; please use Chat Completions instead.
|
||||
*/
|
||||
create(key: IntegrationTaskKey, params: Prettify<OpenAI.EditCreateParams>): Promise<OpenAI.Edit> {
|
||||
let properties = [
|
||||
{
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.input) {
|
||||
properties.push({
|
||||
label: "Input",
|
||||
text: truncate(params.input, 40),
|
||||
});
|
||||
}
|
||||
|
||||
properties.push({
|
||||
label: "Instruction",
|
||||
text: truncate(params.instruction, 40),
|
||||
});
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.edits.create(params);
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Create edit",
|
||||
params,
|
||||
properties,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
import { createTaskUsageProperties } from "./taskUtils";
|
||||
|
||||
export class Embeddings {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: OpenAI.EmbeddingCreateParams
|
||||
): Promise<OpenAI.Embeddings.CreateEmbeddingResponse> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.embeddings.create(params);
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Create embedding",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { fileFromString } from "@trigger.dev/integration-kit";
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
|
||||
type CreateFileRequest = {
|
||||
file: string | File;
|
||||
fileName?: string;
|
||||
purpose: string;
|
||||
};
|
||||
|
||||
type CreateFineTuneFileRequest = {
|
||||
fileName: string;
|
||||
examples: {
|
||||
prompt: string;
|
||||
completion: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export class Files {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
create(key: IntegrationTaskKey, params: CreateFileRequest): Promise<OpenAI.Files.FileObject> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
let file: File;
|
||||
|
||||
if (typeof params.file === "string") {
|
||||
file = await fileFromString(params.file, params.fileName ?? "file.txt");
|
||||
} else {
|
||||
file = params.file;
|
||||
}
|
||||
|
||||
return client.files.create({ file, purpose: params.purpose });
|
||||
},
|
||||
{
|
||||
name: "Create file",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Purpose",
|
||||
text: params.purpose,
|
||||
},
|
||||
{
|
||||
label: "Input type",
|
||||
text: typeof params.file === "string" ? "string" : "File",
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
list(key: IntegrationTaskKey): Promise<OpenAI.Files.FileObject[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.files.list();
|
||||
return response.data;
|
||||
},
|
||||
{
|
||||
name: "List files",
|
||||
properties: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createFineTune(
|
||||
key: IntegrationTaskKey,
|
||||
params: CreateFineTuneFileRequest
|
||||
): Promise<OpenAI.Files.FileObject> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const file = await fileFromString(
|
||||
params.examples.map((d) => JSON.stringify(d)).join("\n"),
|
||||
params.fileName
|
||||
);
|
||||
|
||||
return client.files.create({ file, purpose: "fine-tune" });
|
||||
},
|
||||
{
|
||||
name: "Create fine tune file",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Examples",
|
||||
text: params.examples.length.toString(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
|
||||
type SpecificFineTuneRequest = {
|
||||
fineTuneId: string;
|
||||
};
|
||||
|
||||
export class FineTunes {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: OpenAI.FineTuneCreateParams
|
||||
): Promise<OpenAI.FineTunes.FineTune> {
|
||||
let properties = [
|
||||
{
|
||||
label: "Training file",
|
||||
text: params.training_file,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.validation_file) {
|
||||
properties.push({
|
||||
label: "Validation file",
|
||||
text: params.validation_file,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.model) {
|
||||
properties.push({
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
});
|
||||
}
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTunes.create(params);
|
||||
},
|
||||
{
|
||||
name: "Create fine tune",
|
||||
params,
|
||||
properties,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
list(key: IntegrationTaskKey): Promise<OpenAI.FineTunes.FineTune[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.fineTunes.list();
|
||||
return response.data;
|
||||
},
|
||||
{
|
||||
name: "List fine tunes",
|
||||
properties: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
retrieve(
|
||||
key: IntegrationTaskKey,
|
||||
params: SpecificFineTuneRequest
|
||||
): Promise<OpenAI.FineTunes.FineTune> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTunes.retrieve(params.fineTuneId);
|
||||
},
|
||||
{
|
||||
name: "Retrieve fine tune",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tune id",
|
||||
text: params.fineTuneId,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
cancel(
|
||||
key: IntegrationTaskKey,
|
||||
params: SpecificFineTuneRequest
|
||||
): Promise<OpenAI.FineTunes.FineTune> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTunes.cancel(params.fineTuneId);
|
||||
},
|
||||
{
|
||||
name: "Cancel fine tune",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tune id",
|
||||
text: params.fineTuneId,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
listEvents(
|
||||
key: IntegrationTaskKey,
|
||||
params: SpecificFineTuneRequest
|
||||
): Promise<OpenAI.FineTuneEventsListResponse> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTunes.listEvents(params.fineTuneId, { stream: false });
|
||||
},
|
||||
{
|
||||
name: "List fine tune events",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tune id",
|
||||
text: params.fineTuneId,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
jobs = {
|
||||
/**
|
||||
* Creates a job that fine-tunes a specified model from a given dataset.
|
||||
*
|
||||
* Response includes details of the enqueued job including job status and the name
|
||||
* of the fine-tuned models once complete.
|
||||
*
|
||||
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
|
||||
*/
|
||||
create: (
|
||||
key: IntegrationTaskKey,
|
||||
params: OpenAI.FineTuning.JobCreateParams
|
||||
): Promise<OpenAI.FineTuning.FineTuningJob> => {
|
||||
let properties = [
|
||||
{
|
||||
label: "File ID",
|
||||
text: params.training_file,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.model) {
|
||||
properties.push({
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.validation_file) {
|
||||
properties.push({
|
||||
label: "Validation file",
|
||||
text: params.validation_file,
|
||||
});
|
||||
}
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTuning.jobs.create(params);
|
||||
},
|
||||
{
|
||||
name: "Create Fine Tuning Job",
|
||||
params,
|
||||
properties,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
retrieve: (
|
||||
key: IntegrationTaskKey,
|
||||
params: { id: string }
|
||||
): Promise<OpenAI.FineTuning.FineTuningJob> => {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTuning.jobs.retrieve(params.id);
|
||||
},
|
||||
{
|
||||
name: "Retrieve Fine Tuning Job",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Job ID",
|
||||
text: params.id,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
cancel: (
|
||||
key: IntegrationTaskKey,
|
||||
params: { id: string }
|
||||
): Promise<OpenAI.FineTuning.FineTuningJob> => {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.fineTuning.jobs.cancel(params.id);
|
||||
},
|
||||
{
|
||||
name: "Cancel Fine Tuning Job",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Job ID",
|
||||
text: params.id,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
listEvents: (
|
||||
key: IntegrationTaskKey,
|
||||
params: { id: string }
|
||||
): Promise<OpenAI.FineTuning.FineTuningJobEvent[]> => {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.fineTuning.jobs.listEvents(params.id);
|
||||
return response.data;
|
||||
},
|
||||
{
|
||||
name: "List Fine Tuning Job Events",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Job ID",
|
||||
text: params.id,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
list: (
|
||||
key: IntegrationTaskKey,
|
||||
params: OpenAI.FineTuning.JobListParams
|
||||
): Promise<OpenAI.FineTuning.FineTuningJob[]> => {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const response = await client.fineTuning.jobs.list(params);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
{
|
||||
name: "List Fine Tuning Jobs",
|
||||
params,
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { fileFromUrl, truncate } from "@trigger.dev/integration-kit";
|
||||
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
import { createTaskUsageProperties } from "./taskUtils";
|
||||
|
||||
export type CreateImageEditRequest = {
|
||||
image: string | File;
|
||||
prompt: string;
|
||||
mask?: string | File;
|
||||
n?: number;
|
||||
size?: "256x256" | "512x512" | "1024x1024";
|
||||
response_format?: "url" | "b64_json";
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export type CreateImageVariationRequest = {
|
||||
image: string | File;
|
||||
n?: number;
|
||||
size?: "256x256" | "512x512" | "1024x1024";
|
||||
response_format?: "url" | "b64_json";
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export class Images {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
generate(
|
||||
key: IntegrationTaskKey,
|
||||
params: Prettify<OpenAI.Images.ImageGenerateParams>
|
||||
): Promise<OpenAI.Images.ImagesResponse> {
|
||||
let properties = [
|
||||
{
|
||||
label: "Prompt",
|
||||
text: params.prompt,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.n) {
|
||||
properties.push({
|
||||
label: "Number of images",
|
||||
text: params.n.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.size) {
|
||||
properties.push({
|
||||
label: "Size",
|
||||
text: params.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.response_format) {
|
||||
properties.push({
|
||||
label: "Response format",
|
||||
text: params.response_format,
|
||||
});
|
||||
}
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.images.generate(params);
|
||||
},
|
||||
{
|
||||
name: "Create image",
|
||||
params,
|
||||
properties,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
edit(
|
||||
key: IntegrationTaskKey,
|
||||
params: CreateImageEditRequest
|
||||
): Promise<OpenAI.Images.ImagesResponse> {
|
||||
let properties = [];
|
||||
|
||||
properties.push({
|
||||
label: "Prompt",
|
||||
text: params.prompt,
|
||||
});
|
||||
|
||||
if (params.n) {
|
||||
properties.push({
|
||||
label: "Number of images",
|
||||
text: params.n.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.size) {
|
||||
properties.push({
|
||||
label: "Size",
|
||||
text: params.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.response_format) {
|
||||
properties.push({
|
||||
label: "Response format",
|
||||
text: params.response_format,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof params.image === "string") {
|
||||
properties.push({
|
||||
label: "Image URL",
|
||||
text: params.image,
|
||||
url: params.image,
|
||||
});
|
||||
}
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const file =
|
||||
typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
|
||||
const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask;
|
||||
|
||||
const response = await client.images.edit({
|
||||
image: file,
|
||||
prompt: params.prompt,
|
||||
mask: mask,
|
||||
n: params.n,
|
||||
size: params.size,
|
||||
response_format: params.response_format,
|
||||
user: params.user,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Create image edit",
|
||||
params,
|
||||
properties,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createVariation(
|
||||
key: IntegrationTaskKey,
|
||||
params: CreateImageVariationRequest
|
||||
): Promise<OpenAI.Images.ImagesResponse> {
|
||||
let properties = [];
|
||||
|
||||
if (params.n) {
|
||||
properties.push({
|
||||
label: "Number of images",
|
||||
text: params.n.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.size) {
|
||||
properties.push({
|
||||
label: "Size",
|
||||
text: params.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.response_format) {
|
||||
properties.push({
|
||||
label: "Response format",
|
||||
text: params.response_format,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof params.image === "string") {
|
||||
properties.push({
|
||||
label: "Image URL",
|
||||
text: params.image,
|
||||
url: params.image,
|
||||
});
|
||||
}
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const file =
|
||||
typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
|
||||
|
||||
const response = await client.images.createVariation({
|
||||
image: file,
|
||||
n: params.n,
|
||||
size: params.size,
|
||||
response_format: params.response_format,
|
||||
user: params.user,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
{
|
||||
name: "Create image variation",
|
||||
params,
|
||||
properties,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,32 @@
|
||||
import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
|
||||
import {
|
||||
retry,
|
||||
type ConnectionAuth,
|
||||
type IO,
|
||||
type IOTask,
|
||||
type IntegrationTaskKey,
|
||||
type Json,
|
||||
type RunTaskErrorCallback,
|
||||
type RunTaskOptions,
|
||||
type TriggerIntegration,
|
||||
} from "@trigger.dev/sdk";
|
||||
import OpenAIApi from "openai";
|
||||
import * as tasks from "./tasks";
|
||||
import { Models } from "./models";
|
||||
import { OpenAIIntegrationOptions } from "./types";
|
||||
import { Completions } from "./completions";
|
||||
import { Chat } from "./chat";
|
||||
import { Edits } from "./edits";
|
||||
import { Images } from "./images";
|
||||
import { Embeddings } from "./embeddings";
|
||||
import { Files } from "./files";
|
||||
import { FineTunes } from "./fineTunes";
|
||||
|
||||
export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, typeof tasks>> {
|
||||
client: IntegrationClient<OpenAIApi, typeof tasks>;
|
||||
export type OpenAIRunTask = InstanceType<typeof OpenAI>["runTask"];
|
||||
|
||||
export class OpenAI implements TriggerIntegration {
|
||||
private _options: OpenAIIntegrationOptions;
|
||||
private _client?: OpenAIApi;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
/**
|
||||
* The native OpenAIApi client. This is exposed for use outside of Trigger.dev jobs
|
||||
@@ -28,20 +50,27 @@ export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, t
|
||||
throw `Can't create OpenAI integration (${options.id}) as apiKey was undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.native = new OpenAIApi({
|
||||
apiKey: options.apiKey,
|
||||
organization: options.organization,
|
||||
});
|
||||
}
|
||||
|
||||
this.client = {
|
||||
tasks,
|
||||
usesLocalAuth: true,
|
||||
client: this.native,
|
||||
auth: {
|
||||
apiKey: options.apiKey,
|
||||
organization: options.organization,
|
||||
},
|
||||
};
|
||||
get authSource() {
|
||||
return "LOCAL" as const;
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const openai = new OpenAI(this._options);
|
||||
openai._io = io;
|
||||
openai._connectionKey = connectionKey;
|
||||
openai._client = new OpenAIApi({
|
||||
apiKey: this._options.apiKey,
|
||||
organization: this._options.organization,
|
||||
});
|
||||
return openai;
|
||||
}
|
||||
|
||||
get id() {
|
||||
@@ -51,4 +80,102 @@ export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, t
|
||||
get metadata() {
|
||||
return { id: "openai", name: "OpenAI" };
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: OpenAIApi, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "openai",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
|
||||
get models() {
|
||||
return new Models(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get completions() {
|
||||
return new Completions(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get chat() {
|
||||
return new Chat(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get edits() {
|
||||
return new Edits(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get images() {
|
||||
return new Images(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get embeddings() {
|
||||
return new Embeddings(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get files() {
|
||||
return new Files(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get fineTunes() {
|
||||
return new FineTunes(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
// this provides backwards compatibility for the old API
|
||||
retrieveModel = this.models.retrieve;
|
||||
listModels = this.models.list;
|
||||
deleteModel = this.models.delete;
|
||||
deleteFineTune = this.models.delete;
|
||||
createCompletion = this.completions.create;
|
||||
backgroundCreateCompletion = this.completions.backgroundCreate;
|
||||
createChatCompletion = this.chat.completions.create;
|
||||
backgroundCreateChatCompletion = this.chat.completions.backgroundCreate;
|
||||
|
||||
/**
|
||||
* @deprecated The Edits API is deprecated; please use Chat Completions instead.
|
||||
*/
|
||||
createEdit = this.edits.create;
|
||||
generateImage = this.images.generate;
|
||||
createImage = this.images.generate;
|
||||
createImageEdit = this.images.edit;
|
||||
createImageVariation = this.images.createVariation;
|
||||
createEmbedding = this.embeddings.create;
|
||||
createFile = this.files.create;
|
||||
listFiles = this.files.list;
|
||||
createFineTuneFile = this.files.createFineTune;
|
||||
createFineTune = this.fineTunes.create;
|
||||
listFineTunes = this.fineTunes.list;
|
||||
retrieveFineTune = this.fineTunes.retrieve;
|
||||
cancelFineTune = this.fineTunes.cancel;
|
||||
listFineTuneEvents = this.fineTunes.listEvents;
|
||||
|
||||
/**
|
||||
* Creates a job that fine-tunes a specified model from a given dataset.
|
||||
*
|
||||
* Response includes details of the enqueued job including job status and the name
|
||||
* of the fine-tuned models once complete.
|
||||
*
|
||||
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
|
||||
*/
|
||||
createFineTuningJob = this.fineTunes.jobs.create;
|
||||
retrieveFineTuningJob = this.fineTunes.jobs.retrieve;
|
||||
cancelFineTuningJob = this.fineTunes.jobs.cancel;
|
||||
listFineTuningJobEvents = this.fineTunes.jobs.listEvents;
|
||||
listFineTuningJobs = this.fineTunes.jobs.list;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Model } from "openai/resources";
|
||||
import { OpenAIRunTask } from "./index";
|
||||
import OpenAI from "openai";
|
||||
|
||||
type DeleteFineTunedModelRequest = {
|
||||
fineTunedModelId: string;
|
||||
};
|
||||
export class Models {
|
||||
runTask: OpenAIRunTask;
|
||||
|
||||
constructor(runTask: OpenAIRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
retrieve(key: IntegrationTaskKey, params: { model: string }): Promise<Model> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
return client.models.retrieve(params.model);
|
||||
},
|
||||
{
|
||||
name: "Retrieve model",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Model id",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
list(key: IntegrationTaskKey): Promise<Model[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const result = await client.models.list();
|
||||
return result.data;
|
||||
},
|
||||
{
|
||||
name: "List models",
|
||||
properties: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
delete(
|
||||
key: IntegrationTaskKey,
|
||||
params: DeleteFineTunedModelRequest
|
||||
): Promise<OpenAI.Models.ModelDeleted> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
return client.models.del(params.fineTunedModelId);
|
||||
},
|
||||
{
|
||||
name: "Delete fine tune model",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tuned model id",
|
||||
text: params.fineTunedModelId,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,881 +0,0 @@
|
||||
import type { AuthenticatedTask } from "@trigger.dev/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { OpenAIIntegrationAuth } from "./types";
|
||||
import { redactString } from "@trigger.dev/sdk";
|
||||
import { Prettify, fileFromString, fileFromUrl, truncate } from "@trigger.dev/integration-kit";
|
||||
import { createTaskUsageProperties, onTaskError } from "./taskUtils";
|
||||
|
||||
type OpenAIClientType = InstanceType<typeof OpenAI>;
|
||||
|
||||
export const retrieveModel: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
{ model: string },
|
||||
OpenAI.Models.Model
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return await client.models.retrieve(params.model);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Retrieve model",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Model id",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const listModels: AuthenticatedTask<OpenAIClientType, void, OpenAI.Models.Model[]> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
const response = await client.models.list();
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "List models",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const createCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.CompletionCreateParamsNonStreaming>,
|
||||
OpenAI.Completion
|
||||
> = {
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.completions.create(params);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const backgroundCreateCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.CompletionCreateParamsNonStreaming>,
|
||||
Prettify<OpenAI.Completion>,
|
||||
OpenAIIntegrationAuth
|
||||
> = {
|
||||
run: async (params, client, task, io, auth) => {
|
||||
const response = await io.backgroundFetch<OpenAI.Completion>(
|
||||
"background",
|
||||
"https://api.openai.com/v1/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: redactString`Bearer ${auth.apiKey}`,
|
||||
...(auth.organization ? { "OpenAI-Organization": auth.organization } : {}),
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
}
|
||||
);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Background Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const createChatCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.Chat.CompletionCreateParamsNonStreaming>,
|
||||
Prettify<OpenAI.Chat.ChatCompletion>
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.chat.completions.create(params);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Chat Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const backgroundCreateChatCompletion: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.Chat.CompletionCreateParamsNonStreaming>,
|
||||
Prettify<OpenAI.Chat.ChatCompletion>,
|
||||
OpenAIIntegrationAuth
|
||||
> = {
|
||||
run: async (params, client, task, io, auth) => {
|
||||
const response = await io.backgroundFetch<OpenAI.Chat.ChatCompletion>(
|
||||
"background",
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: redactString`Bearer ${auth.apiKey}`,
|
||||
...(auth.organization ? { "OpenAI-Organization": auth.organization } : {}),
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
},
|
||||
{
|
||||
"500-599": {
|
||||
strategy: "backoff",
|
||||
limit: 5,
|
||||
minTimeoutInMs: 1000,
|
||||
maxTimeoutInMs: 30000,
|
||||
factor: 1.8,
|
||||
randomize: true,
|
||||
},
|
||||
"429": {
|
||||
strategy: "backoff",
|
||||
limit: 10,
|
||||
minTimeoutInMs: 1000,
|
||||
maxTimeoutInMs: 60000,
|
||||
factor: 2,
|
||||
randomize: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Background Chat Completion",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated The Edits API is deprecated; please use Chat Completions instead.
|
||||
*/
|
||||
export const createEdit: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.EditCreateParams>,
|
||||
OpenAI.Edit
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.edits.create(params);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
let properties = [
|
||||
{
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.input) {
|
||||
properties.push({
|
||||
label: "Input",
|
||||
text: truncate(params.input, 40),
|
||||
});
|
||||
}
|
||||
|
||||
properties.push({
|
||||
label: "Instruction",
|
||||
text: truncate(params.instruction, 40),
|
||||
});
|
||||
|
||||
return {
|
||||
name: "Create edit",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const generateImage: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.Images.ImageGenerateParams>,
|
||||
OpenAI.Images.ImagesResponse
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.images.generate(params);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
let properties = [
|
||||
{
|
||||
label: "Prompt",
|
||||
text: params.prompt,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.n) {
|
||||
properties.push({
|
||||
label: "Number of images",
|
||||
text: params.n.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.size) {
|
||||
properties.push({
|
||||
label: "Size",
|
||||
text: params.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.response_format) {
|
||||
properties.push({
|
||||
label: "Response format",
|
||||
text: params.response_format,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Create image",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const createImage = generateImage;
|
||||
|
||||
export type CreateImageEditRequest = {
|
||||
image: string | File;
|
||||
prompt: string;
|
||||
mask?: string | File;
|
||||
n?: number;
|
||||
size?: "256x256" | "512x512" | "1024x1024";
|
||||
response_format?: "url" | "b64_json";
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export const createImageEdit: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateImageEditRequest>,
|
||||
OpenAI.Images.ImagesResponse
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client, task) => {
|
||||
const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
|
||||
const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask;
|
||||
|
||||
const response = await client.images.edit({
|
||||
image: file,
|
||||
prompt: params.prompt,
|
||||
mask: mask,
|
||||
n: params.n,
|
||||
size: params.size,
|
||||
response_format: params.response_format,
|
||||
user: params.user,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
let properties = [];
|
||||
|
||||
properties.push({
|
||||
label: "Prompt",
|
||||
text: params.prompt,
|
||||
});
|
||||
|
||||
if (params.n) {
|
||||
properties.push({
|
||||
label: "Number of images",
|
||||
text: params.n.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.size) {
|
||||
properties.push({
|
||||
label: "Size",
|
||||
text: params.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.response_format) {
|
||||
properties.push({
|
||||
label: "Response format",
|
||||
text: params.response_format,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof params.image === "string") {
|
||||
properties.push({
|
||||
label: "Image URL",
|
||||
text: params.image,
|
||||
url: params.image,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Create image edit",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export type CreateImageVariationRequest = {
|
||||
image: string | File;
|
||||
n?: number;
|
||||
size?: "256x256" | "512x512" | "1024x1024";
|
||||
response_format?: "url" | "b64_json";
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export const createImageVariation: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateImageVariationRequest>,
|
||||
OpenAI.Images.ImagesResponse
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client, task) => {
|
||||
const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
|
||||
|
||||
const response = await client.images.createVariation({
|
||||
image: file,
|
||||
n: params.n,
|
||||
size: params.size,
|
||||
response_format: params.response_format,
|
||||
user: params.user,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
let properties = [];
|
||||
|
||||
if (params.n) {
|
||||
properties.push({
|
||||
label: "Number of images",
|
||||
text: params.n.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.size) {
|
||||
properties.push({
|
||||
label: "Size",
|
||||
text: params.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.response_format) {
|
||||
properties.push({
|
||||
label: "Response format",
|
||||
text: params.response_format,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof params.image === "string") {
|
||||
properties.push({
|
||||
label: "Image URL",
|
||||
text: params.image,
|
||||
url: params.image,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Create image variation",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const createEmbedding: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.EmbeddingCreateParams>,
|
||||
OpenAI.Embeddings.CreateEmbeddingResponse
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client, task) => {
|
||||
const response = await client.embeddings.create(params);
|
||||
|
||||
task.outputProperties = createTaskUsageProperties(response.usage);
|
||||
|
||||
return response;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create embedding",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
type CreateFileRequest = {
|
||||
file: string | File;
|
||||
fileName?: string;
|
||||
purpose: string;
|
||||
};
|
||||
|
||||
export const createFile: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateFileRequest>,
|
||||
Prettify<OpenAI.Files.FileObject>
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
let file: File;
|
||||
|
||||
if (typeof params.file === "string") {
|
||||
file = await fileFromString(params.file, params.fileName ?? "file.txt");
|
||||
} else {
|
||||
file = params.file;
|
||||
}
|
||||
|
||||
return client.files.create({ file, purpose: params.purpose });
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create file",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Purpose",
|
||||
text: params.purpose,
|
||||
},
|
||||
{
|
||||
label: "Input type",
|
||||
text: typeof params.file === "string" ? "string" : "File",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const listFiles: AuthenticatedTask<OpenAIClientType, void, OpenAI.Files.FileObject[]> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
const response = await client.files.list();
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "List files",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
type CreateFineTuneFileRequest = {
|
||||
fileName: string;
|
||||
examples: {
|
||||
prompt: string;
|
||||
completion: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const createFineTuneFile: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<CreateFineTuneFileRequest>,
|
||||
Prettify<OpenAI.Files.FileObject>
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
const file = await fileFromString(
|
||||
params.examples.map((d) => JSON.stringify(d)).join("\n"),
|
||||
params.fileName
|
||||
);
|
||||
|
||||
return client.files.create({ file, purpose: "fine-tune" });
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Create fine tune file",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Examples",
|
||||
text: params.examples.length.toString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const createFineTune: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.FineTuneCreateParams>,
|
||||
OpenAI.FineTunes.FineTune
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTunes.create(params);
|
||||
},
|
||||
init: (params) => {
|
||||
let properties = [
|
||||
{
|
||||
label: "Training file",
|
||||
text: params.training_file,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.validation_file) {
|
||||
properties.push({
|
||||
label: "Validation file",
|
||||
text: params.validation_file,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.model) {
|
||||
properties.push({
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Create fine tune",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const listFineTunes: AuthenticatedTask<OpenAIClientType, void, OpenAI.FineTunes.FineTune[]> =
|
||||
{
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
const response = await client.fineTunes.list();
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "List fine tunes",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
type SpecificFineTuneRequest = {
|
||||
fineTuneId: string;
|
||||
};
|
||||
|
||||
export const retrieveFineTune: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<SpecificFineTuneRequest>,
|
||||
OpenAI.FineTunes.FineTune
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTunes.retrieve(params.fineTuneId);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Retrieve fine tune",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tune id",
|
||||
text: params.fineTuneId,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const cancelFineTune: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<SpecificFineTuneRequest>,
|
||||
OpenAI.FineTunes.FineTune
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTunes.cancel(params.fineTuneId);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Cancel fine tune",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tune id",
|
||||
text: params.fineTuneId,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const listFineTuneEvents: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<SpecificFineTuneRequest>,
|
||||
OpenAI.FineTuneEventsListResponse
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTunes.listEvents(params.fineTuneId, { stream: false });
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "List fine tune events",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tune id",
|
||||
text: params.fineTuneId,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
type DeleteFineTunedModelRequest = {
|
||||
fineTunedModelId: string;
|
||||
};
|
||||
|
||||
export const deleteFineTune: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<DeleteFineTunedModelRequest>,
|
||||
OpenAI.Models.ModelDeleted
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.models.del(params.fineTunedModelId);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Delete fine tune model",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Fine tuned model id",
|
||||
text: params.fineTunedModelId,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a job that fine-tunes a specified model from a given dataset.
|
||||
*
|
||||
* Response includes details of the enqueued job including job status and the name
|
||||
* of the fine-tuned models once complete.
|
||||
*
|
||||
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
|
||||
*/
|
||||
export const createFineTuningJob: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
Prettify<OpenAI.FineTuning.JobCreateParams>,
|
||||
OpenAI.FineTuning.FineTuningJob
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTuning.jobs.create(params);
|
||||
},
|
||||
init: (params) => {
|
||||
let properties = [
|
||||
{
|
||||
label: "File ID",
|
||||
text: params.training_file,
|
||||
},
|
||||
];
|
||||
|
||||
if (params.model) {
|
||||
properties.push({
|
||||
label: "Model",
|
||||
text: params.model,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.validation_file) {
|
||||
properties.push({
|
||||
label: "Validation file",
|
||||
text: params.validation_file,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Create Fine Tuning Job",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get info about a fine-tuning job.
|
||||
*
|
||||
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
|
||||
*/
|
||||
export const retrieveFineTuningJob: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
{ id: string },
|
||||
OpenAI.FineTuning.FineTuningJob
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTuning.jobs.retrieve(params.id);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Retrieve Fine Tuning Job",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Job ID",
|
||||
text: params.id,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const cancelFineTuningJob: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
{ id: string },
|
||||
OpenAI.FineTuning.FineTuningJob
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
return client.fineTuning.jobs.cancel(params.id);
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "Cancel Fine Tuning Job",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Job ID",
|
||||
text: params.id,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const listFineTuningJobEvents: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
{ id: string },
|
||||
OpenAI.FineTuning.FineTuningJobEvent[]
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
const response = await client.fineTuning.jobs.listEvents(params.id);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "List Fine Tuning Job Events",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [
|
||||
{
|
||||
label: "Job ID",
|
||||
text: params.id,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* List your organization's fine-tuning jobs
|
||||
*/
|
||||
export const listFineTuningJobs: AuthenticatedTask<
|
||||
OpenAIClientType,
|
||||
OpenAI.FineTuning.JobListParams,
|
||||
OpenAI.FineTuning.FineTuningJob[]
|
||||
> = {
|
||||
onError: onTaskError,
|
||||
run: async (params, client) => {
|
||||
const response = await client.fineTuning.jobs.list(params);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
init: (params) => {
|
||||
return {
|
||||
name: "List Fine Tuning Jobs",
|
||||
params,
|
||||
icon: "openai",
|
||||
properties: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,37 @@
|
||||
# @trigger.dev/plain
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0`
|
||||
- `@trigger.dev/sdk@2.1.0`
|
||||
|
||||
## 2.1.0-beta.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.1`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.1`
|
||||
|
||||
## 2.1.0-beta.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Integrations are now simpler and support authentication during webhook registration ([`878da3c0`](https://github.com/triggerdotdev/trigger.dev/commit/878da3c01f0a4dfaf33a1f8943a7ad4eed8b8877))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/integration-kit@2.1.0-beta.0`
|
||||
- `@trigger.dev/sdk@2.1.0-beta.0`
|
||||
|
||||
## 2.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user