Indexing errors don't get displayed anywhere (#605)
* Added EndpointIndex status. Default is PENDING, existing rows are SUCCESS * Made it easier to create a migration SQL file * Created a job-catalog file for misconfigured Jobs that should error when running the CLI * EndpointIndex data and state are now optional * The Environments page now shows the status of the last refresh * Improved the UI about endpoints * Added EndpointIndex error column * WIP making the endpoint indexing more robust * Indexing errors are now surfaced * Improved the error show it shows the job id * Use “performEndpointIndexing” when you create your first endpoint from the UI * Use “performEndpointIndexing” for the recurring endpoint checker * Staging is now auto-indexed every 10 mins too * Use “performEndpointIndexing” for the webhook * Moved the throttling to a util * Removed instructional comments * Created a reusable retry system with exponential backoff * Use p-retry for retrying with backoff * The CLI gets indexing results and displays errors * Improved the indexing error messages and display in the console * Use a pre so the Indexing error is correctly split over multiple lines * Use a db transaction for webhook that triggers endpoint indexing * Tidied up imports * Support older versions of the server * Improved the comment on the misconfigured job * Changeset: When indexing user's jobs errors are now stored and displayed
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
When indexing user's jobs errors are now stored and displayed
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { EndpointIndexStatus } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
export function EndpointIndexStatusIcon({ status }: { status: EndpointIndexStatus }) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return <ClockIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
case "STARTED":
|
||||
return <Spinner className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
case "SUCCESS":
|
||||
return (
|
||||
<CheckCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />
|
||||
);
|
||||
case "FAILURE":
|
||||
return <XCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function EndpointIndexStatusLabel({ status }: { status: EndpointIndexStatus }) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "STARTED":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "SUCCESS":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "FAILURE":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function endpointIndexStatusTitle(status: EndpointIndexStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "Pending";
|
||||
case "STARTED":
|
||||
return "Started";
|
||||
case "SUCCESS":
|
||||
return "Success";
|
||||
case "FAILURE":
|
||||
return "Failure";
|
||||
}
|
||||
}
|
||||
|
||||
export function endpointIndexStatusClassNameColor(status: EndpointIndexStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "text-dimmed";
|
||||
case "STARTED":
|
||||
return "text-blue-500";
|
||||
case "SUCCESS":
|
||||
return "text-green-500";
|
||||
case "FAILURE":
|
||||
return "text-rose-500";
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
export const variantClasses = {
|
||||
info: {
|
||||
@@ -51,8 +52,16 @@ export const variantClasses = {
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
pending: {
|
||||
className: "border-blue-400/20 bg-blue-800/30",
|
||||
icon: <Spinner className="h-5 w-5 shrink-0 " />,
|
||||
textColor: "text-blue-300",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type CalloutVariant = keyof typeof variantClasses;
|
||||
|
||||
export function Callout({
|
||||
children,
|
||||
className,
|
||||
@@ -63,7 +72,7 @@ export function Callout({
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
variant: CalloutVariant;
|
||||
to?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
@@ -2,8 +2,17 @@ import type { z } from "zod";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function FormError({ children, id }: { children: React.ReactNode; id?: string }) {
|
||||
export function FormError({
|
||||
children,
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{children && (
|
||||
@@ -11,7 +20,7 @@ export function FormError({ children, id }: { children: React.ReactNode; id?: st
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="flex items-start gap-0.5"
|
||||
className={cn("flex items-start gap-0.5", className)}
|
||||
>
|
||||
<NamedIcon name="error" className="h-4 w-4 shrink-0 justify-start" />
|
||||
<Paragraph id={id} variant="extra-small" className="text-rose-500">
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const IndexEndpointStatsSchema = z.object({
|
||||
jobs: z.number(),
|
||||
sources: z.number(),
|
||||
dynamicTriggers: z.number(),
|
||||
dynamicSchedules: z.number(),
|
||||
});
|
||||
|
||||
export type IndexEndpointStats = z.infer<typeof IndexEndpointStatsSchema>;
|
||||
|
||||
export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats {
|
||||
return IndexEndpointStatsSchema.parse(stats);
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import type {
|
||||
Endpoint,
|
||||
EndpointIndex,
|
||||
EndpointIndexStatus,
|
||||
RuntimeEnvironment,
|
||||
RuntimeEnvironmentType,
|
||||
} from "@trigger.dev/database";
|
||||
import {
|
||||
EndpointIndexError,
|
||||
EndpointIndexErrorSchema,
|
||||
IndexEndpointStats,
|
||||
parseEndpointIndexStats,
|
||||
} from "@trigger.dev/core";
|
||||
|
||||
export type Client = {
|
||||
slug: string;
|
||||
@@ -34,9 +40,11 @@ export type ClientEndpoint =
|
||||
url: string;
|
||||
indexWebhookPath: string;
|
||||
latestIndex?: {
|
||||
status: EndpointIndexStatus;
|
||||
source: string;
|
||||
updatedAt: Date;
|
||||
stats: IndexEndpointStats;
|
||||
stats?: IndexEndpointStats;
|
||||
error?: EndpointIndexError;
|
||||
};
|
||||
environment: {
|
||||
id: string;
|
||||
@@ -81,9 +89,11 @@ export class EnvironmentsPresenter {
|
||||
indexingHookIdentifier: true,
|
||||
indexings: {
|
||||
select: {
|
||||
status: true,
|
||||
source: true,
|
||||
updatedAt: true,
|
||||
stats: true,
|
||||
error: true,
|
||||
},
|
||||
take: 1,
|
||||
orderBy: {
|
||||
@@ -214,7 +224,7 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [
|
||||
|
||||
function endpointClient(
|
||||
endpoint: Pick<Endpoint, "id" | "slug" | "url" | "indexingHookIdentifier"> & {
|
||||
indexings: Pick<EndpointIndex, "source" | "updatedAt" | "stats">[];
|
||||
indexings: Pick<EndpointIndex, "status" | "source" | "updatedAt" | "stats" | "error">[];
|
||||
},
|
||||
environment: Pick<RuntimeEnvironment, "id" | "apiKey" | "type">,
|
||||
baseUrl: string
|
||||
@@ -227,9 +237,13 @@ function endpointClient(
|
||||
indexWebhookPath: `${baseUrl}/api/v1/endpoints/${environment.id}/${endpoint.slug}/index/${endpoint.indexingHookIdentifier}`,
|
||||
latestIndex: endpoint.indexings[0]
|
||||
? {
|
||||
status: endpoint.indexings[0].status,
|
||||
source: endpoint.indexings[0].source,
|
||||
updatedAt: endpoint.indexings[0].updatedAt,
|
||||
stats: parseEndpointIndexStats(endpoint.indexings[0].stats),
|
||||
error: endpoint.indexings[0].error
|
||||
? EndpointIndexErrorSchema.parse(endpoint.indexings[0].error)
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
environment: environment,
|
||||
|
||||
+49
-11
@@ -6,7 +6,7 @@ import { useEventSource } from "remix-utils";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Callout, CalloutVariant } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
@@ -18,8 +18,14 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader } from "~/components/primitives/Sheet";
|
||||
import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
|
||||
import { endpointStreamingPath } from "~/utils/pathBuilder";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { EndpointIndexStatus, RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
|
||||
import {
|
||||
EndpointIndexStatusIcon,
|
||||
EndpointIndexStatusLabel,
|
||||
endpointIndexStatusTitle,
|
||||
} from "~/components/environments/EndpointIndexStatus";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
|
||||
type ConfigureEndpointSheetProps = {
|
||||
slug: string;
|
||||
@@ -119,15 +125,29 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
method="post"
|
||||
action={`/resources/environments/${endpoint.environment.id}/endpoint/${endpoint.id}`}
|
||||
>
|
||||
<Callout variant="success" className="justiy-between items-center">
|
||||
<Paragraph variant="small" className="grow text-green-200">
|
||||
Endpoint configured. Last refreshed:{" "}
|
||||
{endpoint.latestIndex ? (
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Callout
|
||||
variant="info"
|
||||
icon={
|
||||
<EndpointIndexStatusIcon status={endpoint.latestIndex?.status ?? "PENDING"} />
|
||||
}
|
||||
className="justiy-between items-center"
|
||||
>
|
||||
<div className="flex grow items-center gap-2">
|
||||
<EndpointIndexStatusLabel
|
||||
status={endpoint.latestIndex?.status ?? "PENDING"}
|
||||
/>
|
||||
<Paragraph variant="small" className="grow">
|
||||
Last refreshed:{" "}
|
||||
{endpoint.latestIndex ? (
|
||||
<>
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
@@ -138,6 +158,11 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
{refreshingEndpoint ? "Refreshing" : "Refresh now"}
|
||||
</Button>
|
||||
</Callout>
|
||||
{endpoint.latestIndex?.error && (
|
||||
<FormError className="p-2">
|
||||
<pre>{endpoint.latestIndex.error.message}</pre>
|
||||
</FormError>
|
||||
)}
|
||||
</refreshEndpointFetcher.Form>
|
||||
</div>
|
||||
<div className="max-w-full overflow-hidden">
|
||||
@@ -155,3 +180,16 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function calloutVariantFromStatus(status: EndpointIndexStatus): CalloutVariant {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "pending";
|
||||
case "STARTED":
|
||||
return "pending";
|
||||
case "SUCCESS":
|
||||
return "success";
|
||||
case "FAILURE":
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
+19
-4
@@ -3,11 +3,16 @@ import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import {
|
||||
EndpointIndexStatusIcon,
|
||||
EndpointIndexStatusLabel,
|
||||
} from "~/components/environments/EndpointIndexStatus";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
@@ -38,7 +43,6 @@ import { ProjectParamSchema, projectEnvironmentsStreamingPath } from "~/utils/pa
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { FirstEndpointSheet } from "./FirstEndpointSheet";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
@@ -180,6 +184,7 @@ export default function Page() {
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell>Last refreshed</TableHeaderCell>
|
||||
<TableHeaderCell>Last refresh Status</TableHeaderCell>
|
||||
<TableHeaderCell>Jobs</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -268,7 +273,7 @@ function EndpointRow({
|
||||
<EnvironmentLabel environment={{ type }} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick} colSpan={4} alignment="right">
|
||||
<TableCell onClick={onClick} colSpan={5} alignment="right">
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<span className="text-amber-500">
|
||||
The {environmentTitle({ type })} environment is not configured
|
||||
@@ -290,7 +295,17 @@ function EndpointRow({
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex ? <DateTime date={endpoint.latestIndex.updatedAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats.jobs ?? "–"}</TableCell>
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<EndpointIndexStatusIcon status={endpoint.latestIndex.status} />
|
||||
<EndpointIndexStatusLabel status={endpoint.latestIndex.status} />
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats?.jobs ?? "–"}</TableCell>
|
||||
<TableCellChevron onClick={onClick} />
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
EndpointIndexErrorSchema,
|
||||
GetEndpointIndexResponse,
|
||||
GetEndpointIndexResponseSchema,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
indexId: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: ActionArgs) {
|
||||
if (request.method.toUpperCase() !== "GET") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const parsedParams = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
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;
|
||||
|
||||
const { indexId } = parsedParams.data;
|
||||
|
||||
const endpointIndex = await prisma.endpointIndex.findUnique({
|
||||
where: {
|
||||
id: indexId,
|
||||
endpoint: {
|
||||
environmentId: authenticatedEnv.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!endpointIndex) {
|
||||
logger.info("EndpointIndex not found", { url: request.url });
|
||||
return json({ error: "EndpointIndex not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const parsed = GetEndpointIndexResponseSchema.safeParse(endpointIndex);
|
||||
|
||||
if (!parsed.success) {
|
||||
logger.info("EndpointIndex failed parsing", { errors: parsed.error.issues, endpointIndex });
|
||||
const parseFailResult: GetEndpointIndexResponse = {
|
||||
status: "FAILURE",
|
||||
error: {
|
||||
message: "Invalid endpoint index",
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
return json(parseFailResult, { status: 500 });
|
||||
}
|
||||
|
||||
return json(parsed.data);
|
||||
}
|
||||
+45
-35
@@ -1,7 +1,7 @@
|
||||
import { ActionArgs, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { safeJsonParse } from "~/utils/json";
|
||||
@@ -93,43 +93,53 @@ export class TriggerEndpointIndexHookService {
|
||||
body,
|
||||
});
|
||||
|
||||
const endpoint = await this.#prismaClient.endpoint.findUnique({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
environmentId,
|
||||
slug: endpointSlug,
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
const endpoint = await tx.endpoint.findUnique({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
environmentId,
|
||||
slug: endpointSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error("Endpoint not found");
|
||||
}
|
||||
|
||||
if (endpoint.indexingHookIdentifier !== indexHookIdentifier) {
|
||||
throw new Error("Index hook identifier is invalid");
|
||||
}
|
||||
|
||||
const reason = parseReasonFromBody(body);
|
||||
|
||||
// Index the endpoint in 5 seconds from now
|
||||
await workerQueue.enqueue(
|
||||
"indexEndpoint",
|
||||
{
|
||||
id: endpoint.id,
|
||||
source: "HOOK",
|
||||
reason,
|
||||
sourceData: body,
|
||||
},
|
||||
{
|
||||
runAt: new Date(Date.now() + 5000),
|
||||
maxAttempts:
|
||||
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
|
||||
if (!endpoint) {
|
||||
throw new Error("Endpoint not found");
|
||||
}
|
||||
);
|
||||
|
||||
if (endpoint.indexingHookIdentifier !== indexHookIdentifier) {
|
||||
throw new Error("Index hook identifier is invalid");
|
||||
}
|
||||
|
||||
const reason = parseReasonFromBody(body);
|
||||
|
||||
const index = await tx.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
status: "PENDING",
|
||||
source: "HOOK",
|
||||
reason,
|
||||
sourceData: body,
|
||||
},
|
||||
});
|
||||
|
||||
// Index the endpoint in 5 seconds from now
|
||||
await workerQueue.enqueue(
|
||||
"performEndpointIndexing",
|
||||
{
|
||||
id: index.id,
|
||||
},
|
||||
{
|
||||
runAt: new Date(Date.now() + 5000),
|
||||
maxAttempts:
|
||||
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
|
||||
tx,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-6
@@ -1,11 +1,5 @@
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
CreateEndpointError,
|
||||
CreateEndpointService,
|
||||
} from "~/services/endpoints/createEndpoint.server";
|
||||
import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export class EndpointApi {
|
||||
}
|
||||
|
||||
async indexEndpoint() {
|
||||
const startTimeInMs = performance.now();
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -113,40 +114,13 @@ export class EndpointApi {
|
||||
},
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}`);
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
|
||||
|
||||
if (body) {
|
||||
return {
|
||||
ok: false,
|
||||
error: body.message,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: `Trigger API key is invalid`,
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
|
||||
const data = IndexEndpointResponseSchema.parse(anyBody);
|
||||
const headers = EndpointHeadersSchema.parse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data,
|
||||
headers,
|
||||
} as const;
|
||||
response,
|
||||
headerParser: EndpointHeadersSchema,
|
||||
parser: IndexEndpointResponseSchema,
|
||||
errorParser: ErrorWithStackSchema,
|
||||
durationInMs: Math.floor(performance.now() - startTimeInMs),
|
||||
};
|
||||
}
|
||||
|
||||
async executeJobRequest(options: RunJobBody) {
|
||||
|
||||
@@ -82,12 +82,19 @@ export class CreateEndpointService {
|
||||
},
|
||||
});
|
||||
|
||||
const endpointIndex = await tx.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
status: "PENDING",
|
||||
source: "INTERNAL",
|
||||
},
|
||||
});
|
||||
|
||||
// Kick off process to fetch the jobs for this endpoint
|
||||
await workerQueue.enqueue(
|
||||
"indexEndpoint",
|
||||
"performEndpointIndexing",
|
||||
{
|
||||
id: endpoint.id,
|
||||
source: "INTERNAL",
|
||||
id: endpointIndex.id,
|
||||
},
|
||||
{
|
||||
tx,
|
||||
@@ -96,7 +103,7 @@ export class CreateEndpointService {
|
||||
}
|
||||
);
|
||||
|
||||
return endpoint;
|
||||
return { ...endpoint, endpointIndex };
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import type { EndpointIndexSource } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { findEndpoint } from "~/models/endpoint.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterJobService } from "../jobs/registerJob.server";
|
||||
import { logger } from "../logger.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";
|
||||
import { PerformEndpointIndexService } from "./performEndpointIndexService";
|
||||
|
||||
export class IndexEndpointService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerJobService = new RegisterJobService();
|
||||
#disableJobService = new DisableJobService();
|
||||
#registerSourceServiceV1 = new RegisterSourceServiceV1();
|
||||
#registerSourceServiceV2 = new RegisterSourceServiceV2();
|
||||
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
|
||||
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
@@ -29,220 +15,17 @@ export class IndexEndpointService {
|
||||
reason?: string,
|
||||
sourceData?: any
|
||||
) {
|
||||
const endpoint = await findEndpoint(id);
|
||||
|
||||
// Make a request to the endpoint to fetch a list of jobs
|
||||
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url);
|
||||
|
||||
const indexResponse = await client.indexEndpoint();
|
||||
|
||||
if (!indexResponse.ok) {
|
||||
throw new Error(indexResponse.error);
|
||||
}
|
||||
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data;
|
||||
const { "trigger-version": triggerVersion } = indexResponse.headers;
|
||||
|
||||
logger.debug("Indexing endpoint", {
|
||||
endpointId: endpoint.id,
|
||||
endpointUrl: endpoint.url,
|
||||
endpointSlug: endpoint.slug,
|
||||
source: source,
|
||||
sourceData: sourceData,
|
||||
triggerVersion,
|
||||
stats: {
|
||||
jobs: jobs.length,
|
||||
sources: sources.length,
|
||||
dynamicTriggers: dynamicTriggers.length,
|
||||
dynamicSchedules: dynamicSchedules.length,
|
||||
},
|
||||
});
|
||||
|
||||
if (triggerVersion && triggerVersion !== endpoint.version) {
|
||||
await this.#prismaClient.endpoint.update({
|
||||
where: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
data: {
|
||||
version: triggerVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const indexStats = {
|
||||
jobs: 0,
|
||||
sources: 0,
|
||||
dynamicTriggers: 0,
|
||||
dynamicSchedules: 0,
|
||||
disabledJobs: 0,
|
||||
};
|
||||
|
||||
const existingJobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
projectId: endpoint.projectId,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
aliases: {
|
||||
where: {
|
||||
name: "latest",
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
include: {
|
||||
version: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const job of jobs) {
|
||||
if (!job.enabled) {
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, { slug: job.id, version: job.version })
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const registeredVersion = await this.#registerJobService.call(endpoint, job);
|
||||
|
||||
if (registeredVersion) {
|
||||
indexStats.jobs++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to register job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
|
||||
const missingJobs = existingJobs.filter((job) => {
|
||||
return !jobs.find((j) => j.id === job.slug);
|
||||
});
|
||||
|
||||
if (missingJobs.length > 0) {
|
||||
logger.debug("Disabling missing jobs", {
|
||||
endpointId: endpoint.id,
|
||||
missingJobIds: missingJobs.map((job) => job.slug),
|
||||
});
|
||||
|
||||
for (const job of missingJobs) {
|
||||
const latestVersion = job.aliases[0]?.version;
|
||||
|
||||
if (!latestVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, {
|
||||
slug: job.slug,
|
||||
version: latestVersion.version,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
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) {
|
||||
logger.error("Failed to register source", {
|
||||
endpointId: endpoint.id,
|
||||
source,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicTrigger of dynamicTriggers) {
|
||||
try {
|
||||
await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger);
|
||||
|
||||
indexStats.dynamicTriggers++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic trigger", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicTrigger,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicSchedule of dynamicSchedules) {
|
||||
try {
|
||||
await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule);
|
||||
|
||||
indexStats.dynamicSchedules++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic schedule", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicSchedule,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Endpoint indexing complete", {
|
||||
endpointId: endpoint.id,
|
||||
indexStats,
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
});
|
||||
|
||||
return await this.#prismaClient.endpointIndex.create({
|
||||
const endpointIndex = await this.#prismaClient.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
stats: indexStats,
|
||||
data: {
|
||||
jobs,
|
||||
sources,
|
||||
dynamicTriggers,
|
||||
dynamicSchedules,
|
||||
},
|
||||
endpointId: id,
|
||||
status: "PENDING",
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
sourceData,
|
||||
},
|
||||
});
|
||||
|
||||
const performEndpointIndexService = new PerformEndpointIndexService();
|
||||
return await performEndpointIndexService.call(endpointIndex.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import type { EndpointIndexSource } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { findEndpoint } from "~/models/endpoint.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterJobService } from "../jobs/registerJob.server";
|
||||
import { logger } from "../logger.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";
|
||||
import { EndpointIndexError } from "@trigger.dev/core";
|
||||
import { safeBodyFromResponse } from "~/utils/json";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { IndexEndpointStats } from "@trigger.dev/core";
|
||||
|
||||
export class PerformEndpointIndexService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerJobService = new RegisterJobService();
|
||||
#disableJobService = new DisableJobService();
|
||||
#registerSourceServiceV1 = new RegisterSourceServiceV1();
|
||||
#registerSourceServiceV2 = new RegisterSourceServiceV2();
|
||||
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
|
||||
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(id: string) {
|
||||
const endpointIndex = await this.#prismaClient.endpointIndex.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "STARTED",
|
||||
},
|
||||
include: {
|
||||
endpoint: {
|
||||
include: {
|
||||
environment: {
|
||||
include: {
|
||||
organization: true,
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("Performing endpoint index", endpointIndex);
|
||||
|
||||
// Make a request to the endpoint to fetch a list of jobs
|
||||
const client = new EndpointApi(
|
||||
endpointIndex.endpoint.environment.apiKey,
|
||||
endpointIndex.endpoint.url
|
||||
);
|
||||
const { response, parser, headerParser, errorParser } = await client.indexEndpoint();
|
||||
|
||||
if (!response) {
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: `Could not connect to endpoint ${endpointIndex.endpoint.url}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
const body = await safeBodyFromResponse(response, errorParser);
|
||||
|
||||
if (body) {
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: body.message,
|
||||
});
|
||||
}
|
||||
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: "Trigger API key is invalid",
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: `Could not connect to endpoint ${endpointIndex.endpoint.url}. Status code: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
const bodyResult = parser.safeParse(anyBody);
|
||||
|
||||
if (!bodyResult.success) {
|
||||
const issues: string[] = [];
|
||||
bodyResult.error.issues.forEach((issue) => {
|
||||
if (issue.path.at(0) === "jobs") {
|
||||
const jobIndex = issue.path.at(1) as number;
|
||||
const job = (anyBody as any).jobs[jobIndex];
|
||||
|
||||
if (job) {
|
||||
issues.push(`Job "${job.id}": ${issue.message} at "${issue.path.slice(2).join(".")}".`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let friendlyError: string | undefined;
|
||||
if (issues.length > 0) {
|
||||
friendlyError = `Your Jobs have issues:\n${issues.map((issue) => `- ${issue}`).join("\n")}`;
|
||||
} else {
|
||||
friendlyError = fromZodError(bodyResult.error, {
|
||||
prefix: "There's an issue with the format of your Jobs",
|
||||
}).message;
|
||||
}
|
||||
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: friendlyError,
|
||||
raw: bodyResult.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const headerResult = headerParser.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
if (!headerResult.success) {
|
||||
const friendlyError = fromZodError(headerResult.error, {
|
||||
prefix: "Your headers are invalid",
|
||||
});
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: friendlyError.message,
|
||||
raw: headerResult.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } = bodyResult.data;
|
||||
const { "trigger-version": triggerVersion } = headerResult.data;
|
||||
const { endpoint } = endpointIndex;
|
||||
|
||||
if (triggerVersion && triggerVersion !== endpoint.version) {
|
||||
await this.#prismaClient.endpoint.update({
|
||||
where: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
data: {
|
||||
version: triggerVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const indexStats: IndexEndpointStats = {
|
||||
jobs: 0,
|
||||
sources: 0,
|
||||
dynamicTriggers: 0,
|
||||
dynamicSchedules: 0,
|
||||
disabledJobs: 0,
|
||||
};
|
||||
|
||||
const existingJobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
projectId: endpoint.projectId,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
aliases: {
|
||||
where: {
|
||||
name: "latest",
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
include: {
|
||||
version: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const job of jobs) {
|
||||
if (!job.enabled) {
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, { slug: job.id, version: job.version })
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const registeredVersion = await this.#registerJobService.call(endpoint, job);
|
||||
|
||||
if (registeredVersion) {
|
||||
if (!job.internal) {
|
||||
indexStats.jobs++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to register job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
|
||||
const missingJobs = existingJobs.filter((job) => {
|
||||
return !jobs.find((j) => j.id === job.slug);
|
||||
});
|
||||
|
||||
if (missingJobs.length > 0) {
|
||||
logger.debug("Disabling missing jobs", {
|
||||
endpointId: endpoint.id,
|
||||
missingJobIds: missingJobs.map((job) => job.slug),
|
||||
});
|
||||
|
||||
for (const job of missingJobs) {
|
||||
const latestVersion = job.aliases[0]?.version;
|
||||
|
||||
if (!latestVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, {
|
||||
slug: job.slug,
|
||||
version: latestVersion.version,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
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) {
|
||||
logger.error("Failed to register source", {
|
||||
endpointId: endpoint.id,
|
||||
source,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicTrigger of dynamicTriggers) {
|
||||
try {
|
||||
await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger);
|
||||
|
||||
indexStats.dynamicTriggers++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic trigger", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicTrigger,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicSchedule of dynamicSchedules) {
|
||||
try {
|
||||
await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule);
|
||||
|
||||
indexStats.dynamicSchedules++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic schedule", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicSchedule,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Endpoint indexing complete", {
|
||||
endpointId: endpoint.id,
|
||||
indexStats,
|
||||
source: endpointIndex.source,
|
||||
sourceData: endpointIndex.sourceData,
|
||||
reason: endpointIndex.reason,
|
||||
});
|
||||
|
||||
return await this.#prismaClient.endpointIndex.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "SUCCESS",
|
||||
stats: indexStats,
|
||||
data: {
|
||||
jobs,
|
||||
sources,
|
||||
dynamicTriggers,
|
||||
dynamicSchedules,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateEndpointIndexWithError(
|
||||
prismaClient: PrismaClient,
|
||||
id: string,
|
||||
error: EndpointIndexError
|
||||
) {
|
||||
return await prismaClient.endpointIndex.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "FAILURE",
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,9 @@ export class RecurringEndpointIndexService {
|
||||
const endpoints = await this.#prismaClient.endpoint.findMany({
|
||||
where: {
|
||||
environment: {
|
||||
type: RuntimeEnvironmentType.PRODUCTION,
|
||||
type: {
|
||||
in: [RuntimeEnvironmentType.PRODUCTION, RuntimeEnvironmentType.STAGING],
|
||||
},
|
||||
},
|
||||
indexings: {
|
||||
none: {
|
||||
@@ -32,12 +34,18 @@ export class RecurringEndpointIndexService {
|
||||
logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", {
|
||||
count: endpoints.length,
|
||||
});
|
||||
|
||||
// Enqueue each endpoint for indexing
|
||||
for (const endpoint of endpoints) {
|
||||
await workerQueue.enqueue("indexEndpoint", {
|
||||
id: endpoint.id,
|
||||
source: "INTERNAL",
|
||||
const index = await this.#prismaClient.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
status: "PENDING",
|
||||
source: "INTERNAL",
|
||||
},
|
||||
});
|
||||
|
||||
await workerQueue.enqueue("performEndpointIndexing", {
|
||||
id: index.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +66,15 @@ export class ValidateCreateEndpointService {
|
||||
},
|
||||
});
|
||||
|
||||
// Kick off process to fetch the jobs for this endpoint
|
||||
const index = await tx.endpointIndex.create({
|
||||
data: { endpointId: endpoint.id, status: "PENDING", source: "INTERNAL" },
|
||||
});
|
||||
|
||||
// Kick off process to fetch the jobs for this index
|
||||
await workerQueue.enqueue(
|
||||
"indexEndpoint",
|
||||
"performEndpointIndexing",
|
||||
{
|
||||
id: endpoint.id,
|
||||
source: "INTERNAL",
|
||||
id: index.id,
|
||||
},
|
||||
{
|
||||
tx,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { DeliverEmailSchema } from "@/../../packages/emails/src";
|
||||
import { ScheduledPayloadSchema } from "@trigger.dev/core";
|
||||
import { ScheduledPayloadSchema, addMissingVersionField } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { ZodWorker } from "~/platform/zodWorker.server";
|
||||
import { sendEmail, sendPlainTextEmail } from "./email.server";
|
||||
import { sendEmail } from "./email.server";
|
||||
import { IndexEndpointService } from "./endpoints/indexEndpoint.server";
|
||||
import { PerformEndpointIndexService } from "./endpoints/performEndpointIndexService";
|
||||
import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server";
|
||||
import { DeliverEventService } from "./events/deliverEvent.server";
|
||||
import { InvokeDispatcherService } from "./events/invokeDispatcher.server";
|
||||
import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server";
|
||||
import { IntegrationConnectionCreatedService } from "./externalApis/integrationConnectionCreated.server";
|
||||
import { logger } from "./logger.server";
|
||||
import { MissingConnectionCreatedService } from "./runs/missingConnectionCreated.server";
|
||||
import { PerformRunExecutionV1Service } from "./runs/performRunExecutionV1.server";
|
||||
import { PerformRunExecutionV2Service } from "./runs/performRunExecutionV2.server";
|
||||
@@ -20,8 +22,6 @@ import { ActivateSourceService } from "./sources/activateSource.server";
|
||||
import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server";
|
||||
import { PerformTaskOperationService } from "./tasks/performTaskOperation.server";
|
||||
import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout";
|
||||
import { addMissingVersionField } from "@trigger.dev/core";
|
||||
import { logger } from "./logger.server";
|
||||
|
||||
const workerCatalog = {
|
||||
indexEndpoint: z.object({
|
||||
@@ -30,6 +30,9 @@ const workerCatalog = {
|
||||
sourceData: z.any().optional(),
|
||||
reason: z.string().optional(),
|
||||
}),
|
||||
performEndpointIndexing: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
scheduleEmail: DeliverEmailSchema,
|
||||
startRun: z.object({ id: z.string() }),
|
||||
processCallbackTimeout: z.object({
|
||||
@@ -284,10 +287,17 @@ function getWorkerQueue() {
|
||||
maxAttempts: 7,
|
||||
handler: async (payload, job) => {
|
||||
const service = new IndexEndpointService();
|
||||
|
||||
await service.call(payload.id, payload.source, payload.reason, payload.sourceData);
|
||||
},
|
||||
},
|
||||
performEndpointIndexing: {
|
||||
priority: 1, // smaller number = higher priority
|
||||
maxAttempts: 7,
|
||||
handler: async (payload, job) => {
|
||||
const service = new PerformEndpointIndexService();
|
||||
await service.call(payload.id);
|
||||
},
|
||||
},
|
||||
deliverEvent: {
|
||||
priority: 0, // smaller number = higher priority
|
||||
maxAttempts: 5,
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"ulid": "^2.3.0",
|
||||
"zod": "3.22.3",
|
||||
"zod-error": "1.5.0"
|
||||
"zod-error": "1.5.0",
|
||||
"zod-validation-error": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "1.19.2-pre.0",
|
||||
|
||||
@@ -56,11 +56,13 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@types/degit": "^2.8.3",
|
||||
"boxen": "^7.1.1",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"commander": "^9.4.1",
|
||||
"console-table-printer": "^2.11.2",
|
||||
"degit": "^2.8.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"execa": "^7.0.0",
|
||||
@@ -74,6 +76,7 @@
|
||||
"npm-check-updates": "^16.12.2",
|
||||
"openai": "^4.5.0",
|
||||
"ora": "^6.1.2",
|
||||
"p-retry": "^6.1.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"posthog-node": "^3.1.1",
|
||||
"proxy-agent": "^6.3.0",
|
||||
|
||||
+175
-104
@@ -1,3 +1,4 @@
|
||||
import boxen from "boxen";
|
||||
import chalk from "chalk";
|
||||
import childProcess from "child_process";
|
||||
import chokidar from "chokidar";
|
||||
@@ -5,10 +6,12 @@ import fs from "fs/promises";
|
||||
import ngrok from "ngrok";
|
||||
import { run as ncuRun } from "npm-check-updates";
|
||||
import ora, { Ora } from "ora";
|
||||
import pRetry, { AbortError } from "p-retry";
|
||||
import pathModule from "path";
|
||||
import util from "util";
|
||||
import { z } from "zod";
|
||||
import { Framework, getFramework } from "../frameworks";
|
||||
import { standardWatchFilePaths, standardWatchIgnoreRegex } from "../frameworks/watchConfig";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { getEnvFilename } from "../utils/env";
|
||||
import fetch from "../utils/fetchUseProxy";
|
||||
@@ -17,8 +20,9 @@ import { getUserPackageManager } from "../utils/getUserPkgManager";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { RequireKeys } from "../utils/requiredKeys";
|
||||
import { Throttle } from "../utils/throttle";
|
||||
import { TriggerApi } from "../utils/triggerApi";
|
||||
import { standardWatchIgnoreRegex, standardWatchFilePaths } from "../frameworks/watchConfig";
|
||||
import { wait } from "../utils/wait";
|
||||
|
||||
const asyncExecFile = util.promisify(childProcess.execFile);
|
||||
|
||||
@@ -86,7 +90,7 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
const verifiedEndpoint = await verifyEndpoint(resolvedOptions, endpointId, apiKey, framework);
|
||||
if (!verifiedEndpoint) {
|
||||
logger.error(
|
||||
`✖ [trigger.dev] Failed to find a valid Trigger.dev endpoint. Make sure your app is running and try again.`
|
||||
`✖ [trigger.dev] Your endpoint couldn't be verified. Make sure your app is running and try again. ${resolvedOptions.handlerPath}`
|
||||
);
|
||||
logger.info(` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port.`);
|
||||
telemetryClient.dev.failed("no_server_found", resolvedOptions);
|
||||
@@ -107,83 +111,6 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
const endpointHandlerUrl = `${endpointUrl}${handlerPath}`;
|
||||
telemetryClient.dev.tunnelRunning(path, resolvedOptions);
|
||||
|
||||
const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`);
|
||||
|
||||
//refresh function
|
||||
let hasConnected = false;
|
||||
let attemptCount = 0;
|
||||
const refresh = async () => {
|
||||
connectingSpinner.start();
|
||||
|
||||
const refreshedEndpointId = await getEndpointIdFromPackageJson(resolvedPath, resolvedOptions);
|
||||
|
||||
// Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(resolvedPath, resolvedOptions.envFile);
|
||||
|
||||
if (!apiDetails) {
|
||||
connectingSpinner.fail(`[trigger.dev] Failed to connect: Missing API Key`);
|
||||
logger.info(`Will attempt again on the next file change…`);
|
||||
attemptCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiKey, apiUrl } = apiDetails;
|
||||
const apiClient = new TriggerApi(apiKey, apiUrl);
|
||||
|
||||
const authorizedKey = await apiClient.whoami(apiKey);
|
||||
if (!authorizedKey) {
|
||||
logger.error(
|
||||
`✖ [trigger.dev] The API key you provided is not authorized. Try visiting your dashboard to get a new API key.`
|
||||
);
|
||||
|
||||
telemetryClient.dev.failed("invalid_api_key", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryClient.identify(
|
||||
authorizedKey.organization.id,
|
||||
authorizedKey.project.id,
|
||||
authorizedKey.userId
|
||||
);
|
||||
|
||||
const result = await refreshEndpoint(
|
||||
apiClient,
|
||||
refreshedEndpointId ?? endpointId,
|
||||
endpointHandlerUrl
|
||||
);
|
||||
if (result.success) {
|
||||
attemptCount = 0;
|
||||
connectingSpinner.succeed(
|
||||
`[trigger.dev] 🔄 Refreshed ${refreshedEndpointId ?? endpointId} ${formattedDate.format(
|
||||
new Date(result.data.updatedAt)
|
||||
)}`
|
||||
);
|
||||
|
||||
if (!hasConnected) {
|
||||
hasConnected = true;
|
||||
telemetryClient.dev.connected(path, resolvedOptions);
|
||||
}
|
||||
} else {
|
||||
attemptCount++;
|
||||
|
||||
if (attemptCount === 10 || !result.retryable) {
|
||||
connectingSpinner.fail(`Failed to connect: ${result.error}`);
|
||||
logger.info(`Will attempt again on the next file change…`);
|
||||
attemptCount = 0;
|
||||
|
||||
if (!hasConnected) {
|
||||
telemetryClient.dev.failed("failed_to_connect", resolvedOptions);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = backoff(attemptCount);
|
||||
// console.log(`Attempt: ${attemptCount}`, delay);
|
||||
await wait(delay);
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes to files and refresh endpoints
|
||||
const watchPaths = (framework?.watchFilePaths ?? standardWatchFilePaths).map(
|
||||
(path) => `${resolvedPath}/${path}`
|
||||
@@ -195,12 +122,178 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`);
|
||||
let hasConnected = false;
|
||||
const abortController = new AbortController();
|
||||
|
||||
const r = () => {
|
||||
refresh({
|
||||
endpointId,
|
||||
spinner: connectingSpinner,
|
||||
path: resolvedPath,
|
||||
endpointHandlerUrl,
|
||||
resolvedOptions,
|
||||
hasConnected,
|
||||
abortController,
|
||||
});
|
||||
};
|
||||
|
||||
const throttle = new Throttle(r, throttleTimeMs);
|
||||
|
||||
watcher.on("all", (_event, _path) => {
|
||||
throttle(refresh, throttleTimeMs);
|
||||
throttle.call();
|
||||
});
|
||||
|
||||
//Do initial refresh
|
||||
throttle(refresh, throttleTimeMs);
|
||||
throttle.call();
|
||||
}
|
||||
|
||||
type RefreshOptions = {
|
||||
spinner: Ora;
|
||||
path: string;
|
||||
endpointId: string;
|
||||
endpointHandlerUrl: string;
|
||||
resolvedOptions: ResolvedOptions;
|
||||
hasConnected: boolean;
|
||||
abortController: AbortController;
|
||||
};
|
||||
|
||||
async function refresh(options: RefreshOptions) {
|
||||
//stop any existing refreshes
|
||||
options.abortController.abort();
|
||||
options.abortController = new AbortController();
|
||||
|
||||
// Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(options.path, options.resolvedOptions.envFile);
|
||||
if (!apiDetails) {
|
||||
options.spinner.fail("[trigger.dev] Failed to connect: Missing API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiKey, apiUrl } = apiDetails;
|
||||
const apiClient = new TriggerApi(apiKey, apiUrl);
|
||||
|
||||
try {
|
||||
const index = await pRetry(() => startIndexing({ ...options, apiClient }), {
|
||||
retries: 5,
|
||||
signal: options.abortController.signal,
|
||||
maxTimeout: 5000,
|
||||
});
|
||||
options.spinner.text = `[trigger.dev] Refreshing ${formattedDate.format(index.updatedAt)}`;
|
||||
|
||||
if (!options.hasConnected) {
|
||||
options.hasConnected = true;
|
||||
telemetryClient.dev.connected(options.path, options.resolvedOptions);
|
||||
}
|
||||
|
||||
//this is for backwards-compatibility with older servers
|
||||
if (index.id === undefined) {
|
||||
options.spinner.succeed(`[trigger.dev] Refreshed ${formattedDate.format(index.updatedAt)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
//wait 750ms before attempting to get the indexing result
|
||||
await wait(750);
|
||||
|
||||
const indexResult = await pRetry(() => fetchIndexResult({ indexId: index.id, apiClient }), {
|
||||
//this means we're polling, same distance between each attempt
|
||||
factor: 1,
|
||||
retries: 10,
|
||||
signal: options.abortController.signal,
|
||||
});
|
||||
|
||||
if (indexResult.status === "FAILURE") {
|
||||
options.spinner.fail(
|
||||
`[trigger.dev] Refreshing failed ${formattedDate.format(indexResult.updatedAt)}`
|
||||
);
|
||||
logger.error(
|
||||
boxen(indexResult.error.message, {
|
||||
padding: 1,
|
||||
borderStyle: "double",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
options.spinner.succeed(
|
||||
`[trigger.dev] Refreshed ${formattedDate.format(indexResult.updatedAt)}`
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof AbortError) {
|
||||
options.spinner.fail(e.message);
|
||||
logger.info(` [trigger.dev] Will attempt again on the next file change…`);
|
||||
return;
|
||||
}
|
||||
|
||||
let message: string = "";
|
||||
if (e instanceof Error) {
|
||||
message = e.message;
|
||||
} else {
|
||||
message = "Unknown error";
|
||||
}
|
||||
|
||||
options.spinner.fail(message);
|
||||
logger.info(` [trigger.dev] Will attempt again on the next file change…`);
|
||||
|
||||
if (!options.hasConnected) {
|
||||
telemetryClient.dev.failed("failed_to_connect", options.resolvedOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startIndexing({
|
||||
spinner,
|
||||
path,
|
||||
endpointId,
|
||||
endpointHandlerUrl,
|
||||
resolvedOptions,
|
||||
apiClient,
|
||||
}: RefreshOptions & { apiClient: TriggerApi }) {
|
||||
spinner.start();
|
||||
|
||||
const refreshedEndpointId = await getEndpointIdFromPackageJson(path, resolvedOptions);
|
||||
|
||||
const authorizedKey = await apiClient.whoami();
|
||||
if (!authorizedKey) {
|
||||
telemetryClient.dev.failed("invalid_api_key", resolvedOptions);
|
||||
throw new AbortError(
|
||||
"[trigger.dev] The API key you provided is not authorized. Try visiting your dashboard to get a new API key."
|
||||
);
|
||||
}
|
||||
|
||||
telemetryClient.identify(
|
||||
authorizedKey.organization.id,
|
||||
authorizedKey.project.id,
|
||||
authorizedKey.userId
|
||||
);
|
||||
|
||||
const result = await refreshEndpoint(
|
||||
apiClient,
|
||||
refreshedEndpointId ?? endpointId,
|
||||
endpointHandlerUrl
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return { id: result.data.endpointIndex?.id, updatedAt: new Date(result.data.updatedAt) };
|
||||
}
|
||||
|
||||
async function fetchIndexResult({
|
||||
indexId,
|
||||
apiClient,
|
||||
}: {
|
||||
indexId: string;
|
||||
apiClient: TriggerApi;
|
||||
}) {
|
||||
const result = await apiClient.getEndpointIndex(indexId);
|
||||
|
||||
if (result.status === "STARTED" || result.status === "PENDING") {
|
||||
throw new Error("Indexing is still in progress");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function resolveOptions(
|
||||
@@ -209,7 +302,7 @@ async function resolveOptions(
|
||||
unresolvedOptions: DevCommandOptions
|
||||
): Promise<ResolvedOptions> {
|
||||
if (!framework) {
|
||||
logger.info("Failed to detect framework, using default values");
|
||||
logger.info(" [trigger.dev] Failed to detect framework, using default values");
|
||||
return {
|
||||
port: unresolvedOptions.port ?? 3000,
|
||||
hostname: unresolvedOptions.hostname ?? "localhost",
|
||||
@@ -431,25 +524,3 @@ async function refreshEndpoint(apiClient: TriggerApi, endpointId: string, endpoi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//wait function
|
||||
async function wait(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
//throttle function
|
||||
let throttleTimeout: NodeJS.Timeout | null = null;
|
||||
function throttle(fn: () => any, delay: number) {
|
||||
if (throttleTimeout) {
|
||||
clearTimeout(throttleTimeout);
|
||||
}
|
||||
throttleTimeout = setTimeout(fn, delay);
|
||||
}
|
||||
|
||||
const maximum_backoff = 30;
|
||||
const initial_backoff = 0.2;
|
||||
function backoff(attempt: number) {
|
||||
return Math.min((2 ^ attempt) * initial_backoff, maximum_backoff) * 1000;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export const initCommand = async (options: InitCommandOptions) => {
|
||||
}
|
||||
|
||||
const apiClient = new TriggerApi(apiKey, optionsAfterPrompts.apiUrl);
|
||||
const authorizedKey = await apiClient.whoami(apiKey);
|
||||
const authorizedKey = await apiClient.whoami();
|
||||
|
||||
if (!authorizedKey) {
|
||||
logger.error(
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function whoamiCommand(path: string, anyOptions: any) {
|
||||
}
|
||||
|
||||
const triggerAPI = new TriggerApi(apiDetails.apiKey, apiDetails.apiUrl);
|
||||
const userData = await triggerAPI.whoami(apiDetails.apiKey);
|
||||
const userData = await triggerAPI.whoami();
|
||||
|
||||
loadingSpinner.stop();
|
||||
|
||||
|
||||
@@ -13,4 +13,7 @@ export const logger = {
|
||||
success(...args: unknown[]) {
|
||||
console.log(chalk.green(...args));
|
||||
},
|
||||
table(rows: any) {
|
||||
console.table(rows);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export class Throttle {
|
||||
throttleTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly fn: () => any,
|
||||
private readonly delay: number
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
call() {
|
||||
if (this.throttleTimeout) {
|
||||
clearTimeout(this.throttleTimeout);
|
||||
}
|
||||
this.throttleTimeout = setTimeout(this.fn, this.delay);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import fetch from "./fetchUseProxy";
|
||||
import { z } from "zod";
|
||||
import core from "@trigger.dev/core";
|
||||
const { GetEndpointIndexResponseSchema } = core;
|
||||
|
||||
export type CreateEndpointOptions = {
|
||||
id: string;
|
||||
@@ -16,6 +18,9 @@ export type EndpointData = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
indexingHookIdentifier: string;
|
||||
endpointIndex: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EndpointResponse =
|
||||
@@ -59,12 +64,12 @@ export class TriggerApi {
|
||||
private baseUrl: string
|
||||
) {}
|
||||
|
||||
async whoami(apiKey: string): Promise<WhoamiResponse | undefined> {
|
||||
async whoami(): Promise<WhoamiResponse | undefined> {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/whoami`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -155,6 +160,33 @@ export class TriggerApi {
|
||||
data: data as any as EndpointData,
|
||||
};
|
||||
}
|
||||
|
||||
async getEndpointIndex(indexId: string) {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/endpointindex/${indexId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const body = await response.json();
|
||||
const parsed = GetEndpointIndexResponseSchema.safeParse(body);
|
||||
|
||||
if (parsed.success) {
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "FAILURE" as const,
|
||||
error: {
|
||||
message: `Bad response from Trigger.dev (${response.status})`,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(raw: string | null | undefined): unknown {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export async function wait(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
|
||||
/* EMIT RULES */
|
||||
"outDir": "./dist",
|
||||
"noEmit": true, // TSUP takes care of emitting js for us, in a MUCH faster way
|
||||
|
||||
@@ -296,6 +296,53 @@ export const IndexEndpointResponseSchema = z.object({
|
||||
|
||||
export type IndexEndpointResponse = z.infer<typeof IndexEndpointResponseSchema>;
|
||||
|
||||
export const EndpointIndexErrorSchema = z.object({
|
||||
message: z.string(),
|
||||
raw: z.any().optional(),
|
||||
});
|
||||
|
||||
export type EndpointIndexError = z.infer<typeof EndpointIndexErrorSchema>;
|
||||
|
||||
const IndexEndpointStatsSchema = z.object({
|
||||
jobs: z.number(),
|
||||
sources: z.number(),
|
||||
dynamicTriggers: z.number(),
|
||||
dynamicSchedules: z.number(),
|
||||
disabledJobs: z.number().default(0),
|
||||
});
|
||||
|
||||
export type IndexEndpointStats = z.infer<typeof IndexEndpointStatsSchema>;
|
||||
|
||||
export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats | undefined {
|
||||
if (stats === null || stats === undefined) {
|
||||
return;
|
||||
}
|
||||
return IndexEndpointStatsSchema.parse(stats);
|
||||
}
|
||||
|
||||
export const GetEndpointIndexResponseSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("PENDING"),
|
||||
updatedAt: z.coerce.date(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("STARTED"),
|
||||
updatedAt: z.coerce.date(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("SUCCESS"),
|
||||
stats: IndexEndpointStatsSchema,
|
||||
updatedAt: z.coerce.date(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("FAILURE"),
|
||||
error: EndpointIndexErrorSchema,
|
||||
updatedAt: z.coerce.date(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type GetEndpointIndexResponse = z.infer<typeof GetEndpointIndexResponseSchema>;
|
||||
|
||||
export const EndpointHeadersSchema = z.object({
|
||||
"trigger-version": z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
"scripts": {
|
||||
"generate": "prisma generate",
|
||||
"db:migrate:dev": "prisma migrate dev",
|
||||
"db:migrate:dev:create": "prisma migrate dev --create-only",
|
||||
"db:migrate:deploy": "prisma migrate deploy",
|
||||
"db:studio": "prisma studio",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EndpointIndexStatus" AS ENUM ('PENDING', 'STARTED', 'SUCCESS', 'FAILURE');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "EndpointIndex"
|
||||
ADD COLUMN "status" "EndpointIndexStatus" NOT NULL DEFAULT 'PENDING';
|
||||
|
||||
-- Update all existing rows to be SUCCESS. This isn't correct because some of them have failed, but we don't want them to be PENDING.
|
||||
UPDATE "EndpointIndex"
|
||||
SET
|
||||
"status" = 'SUCCESS';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EndpointIndex" ALTER COLUMN "data" DROP NOT NULL,
|
||||
ALTER COLUMN "stats" DROP NOT NULL;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EndpointIndex" ADD COLUMN "error" JSONB;
|
||||
@@ -391,9 +391,11 @@ model EndpointIndex {
|
||||
source EndpointIndexSource @default(MANUAL)
|
||||
sourceData Json?
|
||||
reason String?
|
||||
status EndpointIndexStatus @default(PENDING)
|
||||
|
||||
data Json
|
||||
stats Json
|
||||
data Json?
|
||||
stats Json?
|
||||
error Json?
|
||||
}
|
||||
|
||||
enum EndpointIndexSource {
|
||||
@@ -403,6 +405,13 @@ enum EndpointIndexSource {
|
||||
HOOK
|
||||
}
|
||||
|
||||
enum EndpointIndexStatus {
|
||||
PENDING
|
||||
STARTED
|
||||
SUCCESS
|
||||
FAILURE
|
||||
}
|
||||
|
||||
model Job {
|
||||
id String @id @default(cuid())
|
||||
slug String
|
||||
@@ -664,8 +673,8 @@ model EventRecord {
|
||||
}
|
||||
|
||||
model JobRun {
|
||||
id String @id @default(cuid())
|
||||
number Int
|
||||
id String @id @default(cuid())
|
||||
number Int
|
||||
internal Boolean @default(false)
|
||||
|
||||
job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
Generated
+46
-13
@@ -198,6 +198,7 @@ importers:
|
||||
ulid: ^2.3.0
|
||||
zod: 3.22.3
|
||||
zod-error: 1.5.0
|
||||
zod-validation-error: ^1.5.0
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
|
||||
'@codemirror/commands': 6.1.3
|
||||
@@ -284,6 +285,7 @@ importers:
|
||||
ulid: 2.3.0
|
||||
zod: 3.22.3
|
||||
zod-error: 1.5.0
|
||||
zod-validation-error: 1.5.0_zod@3.22.3
|
||||
devDependencies:
|
||||
'@remix-run/dev': 1.19.2-pre.0_36n2i74sizt32vwpdxc4husnkq
|
||||
'@remix-run/eslint-config': 1.19.2-pre.0_ol4nhuzbuflsbzk2mijpqykzba
|
||||
@@ -659,6 +661,7 @@ importers:
|
||||
|
||||
packages/cli:
|
||||
specifiers:
|
||||
'@trigger.dev/core': workspace:*
|
||||
'@trigger.dev/tsconfig': workspace:*
|
||||
'@types/degit': ^2.8.3
|
||||
'@types/gradient-string': ^1.1.2
|
||||
@@ -670,6 +673,7 @@ importers:
|
||||
chalk: ^5.2.0
|
||||
chokidar: ^3.5.3
|
||||
commander: ^9.4.1
|
||||
console-table-printer: ^2.11.2
|
||||
degit: ^2.8.4
|
||||
dotenv: ^16.3.1
|
||||
execa: ^7.0.0
|
||||
@@ -683,6 +687,7 @@ importers:
|
||||
npm-check-updates: ^16.12.2
|
||||
openai: ^4.5.0
|
||||
ora: ^6.1.2
|
||||
p-retry: ^6.1.0
|
||||
path-to-regexp: ^6.2.1
|
||||
posthog-node: ^3.1.1
|
||||
proxy-agent: ^6.3.0
|
||||
@@ -697,11 +702,13 @@ importers:
|
||||
vitest: ^0.34.4
|
||||
zod: 3.22.3
|
||||
dependencies:
|
||||
'@trigger.dev/core': link:../core
|
||||
'@types/degit': 2.8.3
|
||||
boxen: 7.1.1
|
||||
chalk: 5.2.0
|
||||
chokidar: 3.5.3
|
||||
commander: 9.5.0
|
||||
console-table-printer: 2.11.2
|
||||
degit: 2.8.4
|
||||
dotenv: 16.3.1
|
||||
execa: 7.0.0
|
||||
@@ -715,6 +722,7 @@ importers:
|
||||
npm-check-updates: 16.12.3
|
||||
openai: 4.5.0
|
||||
ora: 6.1.2
|
||||
p-retry: 6.1.0
|
||||
path-to-regexp: 6.2.1
|
||||
posthog-node: 3.1.1
|
||||
proxy-agent: 6.3.0
|
||||
@@ -13840,6 +13848,10 @@ packages:
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
dev: false
|
||||
|
||||
/@types/retry/0.12.2:
|
||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||
dev: false
|
||||
|
||||
/@types/scheduler/0.16.2:
|
||||
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||
|
||||
@@ -15614,7 +15626,7 @@ packages:
|
||||
/axios/0.21.4_debug@4.3.2:
|
||||
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.2_debug@4.3.2
|
||||
follow-redirects: 1.15.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
@@ -16972,6 +16984,12 @@ packages:
|
||||
/console-control-strings/1.1.0:
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
|
||||
/console-table-printer/2.11.2:
|
||||
resolution: {integrity: sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==}
|
||||
dependencies:
|
||||
simple-wcswidth: 1.0.1
|
||||
dev: false
|
||||
|
||||
/content-disposition/0.5.4:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -20411,18 +20429,6 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
/follow-redirects/1.15.2_debug@4.3.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 4.3.2
|
||||
dev: false
|
||||
|
||||
/for-each/0.3.3:
|
||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||
dependencies:
|
||||
@@ -22174,6 +22180,11 @@ packages:
|
||||
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
/is-network-error/1.0.0:
|
||||
resolution: {integrity: sha512-P3fxi10Aji2FZmHTrMPSNFbNC6nnp4U5juPAIjXPHkUNubi4+qK7vvdsaNpAUwXslhYm9oyjEYTxs1xd/+Ph0w==}
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/is-node-process/1.0.1:
|
||||
resolution: {integrity: sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==}
|
||||
dev: true
|
||||
@@ -25802,6 +25813,15 @@ packages:
|
||||
retry: 0.13.1
|
||||
dev: false
|
||||
|
||||
/p-retry/6.1.0:
|
||||
resolution: {integrity: sha512-fJLEQ2KqYBJRuaA/8cKMnqhulqNM+bpcjYtXNex2t3mOXKRYPitAJt9NacSf8XAFzcYahSAbKpobiWDSqHSh2g==}
|
||||
engines: {node: '>=16.17'}
|
||||
dependencies:
|
||||
'@types/retry': 0.12.2
|
||||
is-network-error: 1.0.0
|
||||
retry: 0.13.1
|
||||
dev: false
|
||||
|
||||
/p-timeout/3.2.0:
|
||||
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -28780,6 +28800,10 @@ packages:
|
||||
semver: 7.5.4
|
||||
dev: true
|
||||
|
||||
/simple-wcswidth/1.0.1:
|
||||
resolution: {integrity: sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==}
|
||||
dev: false
|
||||
|
||||
/simplur/3.0.1:
|
||||
resolution: {integrity: sha512-bBAoTn75tuKh83opmZ1VoyVoQIsvLCKzSxuasAxbnKofrT8eGyOEIaXSuNfhi/hI160+fwsR7ObcbBpOyzDvXg==}
|
||||
dev: false
|
||||
@@ -32595,6 +32619,15 @@ packages:
|
||||
zod: 3.22.3
|
||||
dev: false
|
||||
|
||||
/zod-validation-error/1.5.0_zod@3.22.3:
|
||||
resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.18.0
|
||||
dependencies:
|
||||
zod: 3.22.3
|
||||
dev: false
|
||||
|
||||
/zod/3.21.1:
|
||||
resolution: {integrity: sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA==}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"byo-auth": "nodemon --watch src/byo-auth.ts -r tsconfig-paths/register -r dotenv/config src/byo-auth.ts",
|
||||
"redacted": "nodemon --watch src/redacted.ts -r tsconfig-paths/register -r dotenv/config src/redacted.ts",
|
||||
"replicate": "nodemon --watch src/replicate.ts -r tsconfig-paths/register -r dotenv/config src/replicate.ts",
|
||||
"misconfigured": "nodemon --watch src/misconfigured.ts -r tsconfig-paths/register -r dotenv/config src/misconfigured.ts",
|
||||
"dev:trigger": "trigger-cli dev --port 8080"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -60,4 +61,4 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^3.14.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { TriggerClient, intervalTrigger } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
// This job is misconfigured because the interval trigger is less than 60s
|
||||
client.defineJob({
|
||||
id: "bad-interval",
|
||||
name: "Bad Interval",
|
||||
version: "0.0.2",
|
||||
trigger: intervalTrigger({
|
||||
seconds: 50,
|
||||
}),
|
||||
run: async (payload, io, ctx) => {},
|
||||
});
|
||||
|
||||
// This job is misconfigured because it has no name
|
||||
//@ts-ignore
|
||||
client.defineJob({
|
||||
id: "bad-cron",
|
||||
// name: "Bad CRON expression",
|
||||
version: "0.0.2",
|
||||
trigger: intervalTrigger({
|
||||
seconds: 90,
|
||||
}),
|
||||
run: async (payload, io, ctx) => {},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
Reference in New Issue
Block a user