Compare commits

...

25 Commits

Author SHA1 Message Date
Eric Allam f888a49555 Updated outdated lockfile 2023-09-30 22:20:34 +01:00
Eric Allam 421c249e50 Add some documentation around canceling scheduled events 2023-09-30 22:18:52 +01:00
James Ritchie a8a6f51387 autofocus the search field on the Job page 2023-09-29 18:23:22 +01:00
James Ritchie 12e73eef22 Display framework logos on the onboarding setup pages (#519) 2023-09-29 16:44:35 +01:00
github-actions[bot] 2e33fcb16b chore: Update version for release (#521)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-29 16:42:53 +01:00
Matt Aitken 5912cdd11c Updated the docs for the React hooks 2023-09-29 15:37:53 +01:00
Matt Aitken cc016b3ae3 CLI init: adds public key as “TRIGGER_PUBLIC_API_KEY” except for Next which overrides this 2023-09-29 15:14:52 +01:00
Matt Aitken 7760e09462 Improved CLI init Next.js middleware detection 2023-09-29 11:26:29 +01:00
James Ritchie 3ca4456c88 Youtube embedded video fits its aspect ratio instead of going full width
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-28 17:37:07 +01:00
James Ritchie 618b7f22da Swapped out the Homepage link in the side menu for a link to the Changelog 2023-09-28 17:27:21 +01:00
Eric Allam a12c7c3b0a Fixed sparodically failed run creations
- Use a better way of getting the latest job run number to increment
- Make the CreateRunService transaction more reliable
- Invoke dispatchers in parallel
- No longer swallow prisma errors in $transaction
2023-09-28 17:08:26 +01:00
Eric Allam a42e94c75f Add the STAGING environment by default
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-27 18:17:19 +01:00
Matt Aitken bc757c8ddb Latest lockfile 2023-09-27 18:12:54 +01:00
github-actions[bot] 6e11ab9183 chore: Update version for release (#513)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-27 18:09:16 +01:00
Matt Aitken 2397fcb640 Express frameworks docs + CLI (#512)
* Manual setup docs

* Added the onboarding

* The emails package now works with Node > 18

* Added CLi support for Express, including custom init command finished messages

* Need to actually log out the installation complete message…

* Fix for an old Remix reference

* Renamed the page export

* Use resolvedOptions.triggerUrl

* Typo in manual instructions
2023-09-27 17:44:50 +01:00
Eric Allam 35d0c2a06f Implement the task output redacting to prevent redacted values from showing in the logs 2023-09-27 16:38:30 +01:00
Matt Aitken 813ec74672 Increased the intervalTrigger max from 1 day to 30 days
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-26 17:23:36 +01:00
Matt Aitken 03db13171a Latest lockfile 2023-09-26 16:46:41 +01:00
Matt Aitken 0ecb5129e8 Fix for incorrectly named Next.js package in manual setup 2023-09-26 12:47:49 +01:00
github-actions[bot] 44cb28c1c4 chore: Update version for release (#508)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-26 11:49:03 +01:00
Matt Aitken 4578f6bd64 Astro CLI support (#506)
* Astro framework CLI support

* Changeset: Added Astro automatic installation

* Fixed the package name – was remix, now astro

* Fixed the export of the example

* Added IPv6 localhost to Astro hostnames

* Updated Astro onboarding to show the CLI init command, instead of manual instructions

* Astro quickstart

* Fix for type in Remix quickstart

* Next.js framework detection allows different config file extensions and “next” devDependency

* Made the dev command port more general so it works with various frameworks
2023-09-26 11:44:35 +01:00
Eric Allam 8b25e57613 hotfix 2 2023-09-24 14:43:02 -07:00
Eric Allam 8fb9ea19a3 hotfix 2023-09-24 14:04:46 -07:00
Eric Allam eb4ca0ce2d Fixed duplicate end month
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-09-22 17:24:55 -07:00
Eric Allam df24cd5b71 feat: Basic usage dashboard to show run volume (#501)
* New usage dashboard with static data

* Implement org usage dash

* Grab chart data for the last 12 months

* If no org is found just return undefined so a 404 will be shown

* Remove mock data

* Fill in missing months with 0s

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-09-22 17:02:53 -07:00
111 changed files with 2862 additions and 367 deletions
+1 -1
View File
@@ -106,7 +106,7 @@ export function TriggerDevStep() {
</Paragraph>
<TriggerDevCommand />
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
If youre not running on the default you can specify the port by adding{" "}
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
</Paragraph>
<Paragraph spacing variant="small">
@@ -51,18 +51,18 @@ export function FrameworkSelector() {
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
<NextjsLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupExpressPath(organization, project)}>
<FrameworkLink to={projectSetupExpressPath(organization, project)} supported>
<ExpressLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
<RemixLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
<RedwoodLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
<AstroLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
<RedwoodLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
<NuxtLogo className="w-32" />
</FrameworkLink>
@@ -272,6 +272,21 @@ export function HowToUseApiKeysAndEndpoints() {
you should use the Test feature to trigger any scheduled Jobs.
</Callout>
</StepContentContainer>
<StepNumber
stepNumber="→"
title={
<span className="flex items-center gap-x-2">
<span>Staging</span>
<EnvironmentLabel environment={{ type: "STAGING" }} />
</span>
}
/>
<StepContentContainer>
<Paragraph spacing>
The <InlineCode>STAGING</InlineCode> environment is where your Jobs will run in a staging
environment, meant to mirror your production environment.
</Paragraph>
</StepContentContainer>
<StepNumber
stepNumber="→"
title={
@@ -119,11 +119,11 @@ export function ProjectSideMenu() {
data-action="onboarding"
/>
<SideMenuItem
name="Homepage"
icon="external-link"
to="https://trigger.dev"
name="Changelog"
icon="list"
to="https://trigger.dev/changelog"
isCollapsed={isCollapsed}
data-action="onboarding"
data-action="changelog"
target="_blank"
/>
<SideMenuItem
@@ -131,6 +131,7 @@ const icons = {
"clipboard-checked": (className: string) => (
<ClipboardDocumentCheckIcon className={cn("text-dimmed", className)} />
),
list: (className: string) => <ListBulletIcon className={cn("text-slate-400", className)} />,
log: (className: string) => (
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
),
+7 -5
View File
@@ -31,7 +31,7 @@ export type PrismaTransactionOptions = {
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
rethrowPrismaErrors?: boolean;
swallowPrismaErrors?: boolean;
};
export async function $transaction<R>(
@@ -55,11 +55,9 @@ export async function $transaction<R>(
name: error.name,
});
if (options?.rethrowPrismaErrors) {
throw error;
if (options?.swallowPrismaErrors) {
return;
}
return;
}
throw error;
@@ -124,6 +122,10 @@ function getClient() {
emit: "stdout",
level: "warn",
},
// {
// emit: "stdout",
// level: "query",
// },
],
});
@@ -8,7 +8,6 @@ import type {
import { customAlphabet } from "nanoid";
import slug from "slug";
import { prisma, PrismaClientOrTransaction } from "~/db.server";
import { workerQueue } from "~/services/worker.server";
import { createProject } from "./project.server";
export type { Organization };
@@ -76,6 +75,10 @@ export async function createOrganization(
},
attemptCount = 0
): Promise<Organization & { projects: Project[] }> {
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
throw new Error("Organization could not be created.");
}
const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`;
const orgWithSameSlug = await prisma.organization.findFirst({
@@ -172,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) {
return "prod";
}
case "STAGING": {
return "staging";
return "stg";
}
case "PREVIEW": {
return "preview";
return "prev";
}
}
}
+1
View File
@@ -65,6 +65,7 @@ export async function createProject(
// Create the dev and prod environments
await createEnvironment(organization, project, "PRODUCTION");
await createEnvironment(organization, project, "STAGING");
for (const member of project.organization.members) {
await createEnvironment(organization, project, "DEVELOPMENT", member);
@@ -2,19 +2,19 @@ import { PrismaClient, prisma } from "~/db.server";
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import {
import type {
Endpoint,
EndpointIndex,
RuntimeEnvironment,
RuntimeEnvironmentType,
} from "../../../../packages/database/src";
import { env } from "~/env.server";
} from "@trigger.dev/database";
export type Client = {
slug: string;
endpoints: {
DEVELOPMENT: ClientEndpoint;
PRODUCTION: ClientEndpoint;
STAGING?: ClientEndpoint;
};
};
@@ -133,6 +133,8 @@ export class EnvironmentsPresenter {
throw new Error("Development environment not found, this should not happen");
}
const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING");
const productionEnvironment = filtered.find(
(environment) => environment.type === "PRODUCTION"
);
@@ -151,6 +153,9 @@ export class EnvironmentsPresenter {
state: "unconfigured",
environment: productionEnvironment,
},
STAGING: stagingEnvironment
? { state: "unconfigured", environment: stagingEnvironment }
: undefined,
},
};
@@ -161,6 +166,16 @@ export class EnvironmentsPresenter {
client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl);
}
if (stagingEnvironment) {
const stagingEndpoint = stagingEnvironment.endpoints.find(
(endpoint) => endpoint.slug === slug
);
if (stagingEndpoint) {
client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl);
}
}
const prodEndpoint = productionEnvironment.endpoints.find(
(endpoint) => endpoint.slug === slug
);
@@ -0,0 +1,198 @@
import { PrismaClient, prisma } from "~/db.server";
export class OrgUsagePresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ userId, slug }: { userId: string; slug: string }) {
const organization = await this.#prismaClient.organization.findFirst({
where: {
slug,
members: {
some: {
userId,
},
},
},
});
if (!organization) {
return;
}
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const startOfLastMonth = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1); // this works for January as well
// Get count of runs since the start of the current month
const runsCount = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
},
},
});
// Get the count of runs for last month
const runsCountLastMonth = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: startOfLastMonth,
lt: startOfMonth,
},
},
});
// Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be:
// [
// { month: "2021-01", count: 10 },
// { month: "2021-02", count: 20 },
// { month: "2021-03", count: 30 },
// { month: "2021-04", count: 40 },
// { month: "2021-05", count: 50 },
// { month: "2021-06", count: 60 },
// ]
// This will be used to generate the chart on the usage page
// Use prisma queryRaw for this since prisma doesn't support grouping by month
const chartDataRaw = await this.#prismaClient.$queryRaw<
{
month: string;
count: number;
}[]
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`;
const chartData = chartDataRaw.map((obj) => ({
name: obj.month,
total: Number(obj.count), // Convert BigInt to Number
}));
const totalJobs = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
internal: false,
},
});
const totalJobsLastMonth = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
deletedAt: null,
internal: false,
},
});
const totalIntegrations = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
},
});
const totalIntegrationsLastMonth = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
},
});
const totalMembers = await this.#prismaClient.orgMember.count({
where: {
organizationId: organization.id,
},
});
const jobs = await this.#prismaClient.job.findMany({
where: {
organizationId: organization.id,
deletedAt: null,
internal: false,
},
select: {
id: true,
slug: true,
_count: {
select: {
runs: {
where: {
createdAt: {
gte: startOfMonth,
},
},
},
},
},
project: {
select: {
id: true,
name: true,
slug: true,
},
},
},
});
return {
id: organization.id,
runsCount,
runsCountLastMonth,
chartData: fillInMissingMonthlyData(chartData, 6),
totalJobs,
totalJobsLastMonth,
totalIntegrations,
totalIntegrationsLastMonth,
totalMembers,
jobs,
};
}
}
// This will fill in missing chart data with zeros
// So for example, if data is [{ name: "2021-01", total: 10 }, { name: "2021-03", total: 30 }] and the totalNumberOfMonths is 6
// And the current month is "2021-04", then this function will return:
// [{ name: "2020-11", total: 0 }, { name: "2020-12", total: 0 }, { name: "2021-01", total: 10 }, { name: "2021-02", total: 0 }, { name: "2021-03", total: 30 }, { name: "2021-04", total: 0 }]
function fillInMissingMonthlyData(
data: Array<{ name: string; total: number }>,
totalNumberOfMonths: number
): Array<{ name: string; total: number }> {
const currentMonth = new Date().toISOString().slice(0, 7);
const startMonth = new Date(
new Date(currentMonth).getFullYear(),
new Date(currentMonth).getMonth() - totalNumberOfMonths,
1
)
.toISOString()
.slice(0, 7);
const months = getMonthsBetween(startMonth, currentMonth);
let completeData = months.map((month) => {
let foundData = data.find((d) => d.name === month);
return foundData ? { ...foundData } : { name: month, total: 0 };
});
return completeData;
}
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
const startDate = new Date(startMonth);
const endDate = new Date(endMonth);
const months = [];
let currentDate = startDate;
while (currentDate <= endDate) {
months.push(currentDate.toISOString().slice(0, 7));
currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
}
return months;
}
@@ -1,6 +1,8 @@
import { RedactSchema } from "@trigger.dev/core";
import { StyleSchema } from "@trigger.dev/core";
import { PrismaClient, prisma } from "~/db.server";
import { mergeProperties } from "~/utils/mergeProperties.server";
import { Redactor } from "~/utils/redactor";
type DetailsProps = {
id: string;
@@ -61,6 +63,7 @@ export class TaskDetailsPresenter {
completedAt: true,
style: true,
parentId: true,
redact: true,
attempts: {
select: {
number: true,
@@ -85,11 +88,32 @@ export class TaskDetailsPresenter {
return {
...task,
output: task.output ? JSON.stringify(task.output, null, 2) : undefined,
redact: undefined,
output: task.output
? JSON.stringify(this.#stringifyOutputWithRedactions(task.output, task.redact), null, 2)
: undefined,
connection: task.runConnection,
params: task.params as Record<string, any>,
properties: mergeProperties(task.properties, task.outputProperties),
style: task.style ? StyleSchema.parse(task.style) : undefined,
};
}
#stringifyOutputWithRedactions(output: any, redact: unknown): any {
if (!output) {
return;
}
const parsedRedact = RedactSchema.safeParse(redact);
if (!parsedRedact.success) {
return output;
}
const paths = parsedRedact.data.paths;
const redactor = new Redactor(paths);
return redactor.redact(output);
}
}
@@ -1,17 +1,178 @@
import { ComingSoon } from "~/components/ComingSoon";
import { PageContainer, PageBody } from "~/components/layout/AppLayout";
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import {
ForwardIcon,
SquaresPlusIcon,
UsersIcon,
WrenchScrewdriverIcon,
} from "@heroicons/react/24/solid";
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Header2 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { useOrganization } from "~/hooks/useOrganizations";
import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder";
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
import { Link } from "@remix-run/react/dist/components";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
import { requireUserId } from "~/services/session.server";
export async function loader({ params, request }: LoaderArgs) {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const presenter = new OrgUsagePresenter();
const data = await presenter.call({ userId, slug: organizationSlug });
if (!data) {
throw new Response(null, { status: 404 });
}
return typedjson(data);
}
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
if (active && payload) {
return (
<div className="flex items-center gap-2 rounded border border-border bg-slate-900 px-4 py-2 text-sm text-dimmed">
<p className="text-white">{label}:</p>
<p className="text-white">{payload[0].value}</p>
</div>
);
}
return null;
};
export default function Page() {
const organization = useOrganization();
const loaderData = useTypedLoaderData<typeof loader>();
return (
<PageContainer>
<OrgAdminHeader />
<PageBody>
<ComingSoon
title="Usage & billing"
description="View your usage, tier and billing information. During the beta we will display usage and start billing if you exceed your limits. But don't worry, we'll give you plenty of warning."
icon="billing"
/>
<div className="mb-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Runs this month</Header2>
<ForwardIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.runsCount.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.runsCountLastMonth} runs last month
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Jobs</Header2>
<WrenchScrewdriverIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalJobs.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.totalJobs === loaderData.totalJobsLastMonth ? (
<>No change since last month</>
) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? (
<>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month</>
) : (
<>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Integrations</Header2>
<SquaresPlusIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalIntegrations.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? (
<>No change since last month</>
) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? (
<>
+{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since
last month
</>
) : (
<>
-{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since
last month
</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Team members</Header2>
<UsersIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalMembers.toLocaleString()}</p>
<TextLink
to={organizationTeamPath(organization)}
className="group text-sm text-dimmed hover:text-bright"
>
Manage
<ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" />
</TextLink>
</div>
</div>
</div>
<div className="flex max-h-[500px] gap-x-4">
<div className="w-1/2 rounded border border-border py-6 pr-2">
<Header2 className="mb-8 pl-6">Job Runs per month</Header2>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={loaderData.chartData}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} content={<CustomTooltip />} />
<Bar dataKey="total" fill="#DB2777" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div className="w-1/2 overflow-y-auto rounded border border-border px-3 py-6">
<div className="mb-2 flex items-baseline justify-between border-b border-border px-3 pb-4">
<Header2 className="">Jobs</Header2>
<Header2 className="">Runs</Header2>
</div>
<div className="space-y-2">
{loaderData.jobs.map((job) => (
<Link
to={jobPath(organization, job.project, job)}
className="flex items-center rounded px-4 py-3 transition hover:bg-slate-850"
key={job.id}
>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">{job.slug}</p>
<p className="text-sm text-muted-foreground">Project: {job.project.name}</p>
</div>
<div className="ml-auto font-medium">{job._count.runs.toLocaleString()}</div>
</Link>
))}
</div>
</div>
</div>
</PageBody>
</PageContainer>
);
@@ -104,6 +104,7 @@ export default function Page() {
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
autoFocus
/>
<HelpTrigger title="Example Jobs and inspiration" />
</div>
@@ -160,7 +161,7 @@ function ExampleJobs() {
height="250"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
className="mb-4 w-full border-b border-slate-800"
className="mb-4 border-b border-slate-800"
/>
<Header2 spacing>How to create a Job</Header2>
<Paragraph variant="small" spacing>
@@ -85,8 +85,8 @@ export default function Page() {
const client = clients.find((c) => c.slug === selected.client);
if (!client) return undefined;
if (selected.type === "PREVIEW" || selected.type === "STAGING") {
throw new Error("PREVIEW/STAGING is not yet supported");
if (selected.type === "PREVIEW") {
throw new Error("PREVIEW is not yet supported");
}
return {
@@ -195,6 +195,18 @@ export default function Page() {
})
}
/>
{client.endpoints.STAGING && (
<EndpointRow
endpoint={client.endpoints.STAGING}
type="STAGING"
onClick={() =>
setSelected({
client: client.slug,
type: "STAGING",
})
}
/>
)}
<EndpointRow
endpoint={client.endpoints.PRODUCTION}
type="PRODUCTION"
@@ -218,7 +230,7 @@ export default function Page() {
</>
)}
</div>
{selectedEndpoint && (
{selectedEndpoint && selectedEndpoint.endpoint && (
<ConfigureEndpointSheet
slug={selectedEndpoint.clientSlug}
endpoint={selectedEndpoint.endpoint}
@@ -39,9 +39,14 @@ export default function SetUpAstro() {
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<AstroLogo className="w-64" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
@@ -76,28 +81,16 @@ export default function SetUpAstro() {
<div>
<StepNumber
stepNumber="1"
title="Follow the steps from the Astro manual installation guide"
title="Run the CLI 'init' command in an existing Astro project"
/>
<StepContentContainer className="flex flex-col gap-2">
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
<div className="mb-2 flex w-full items-center justify-between">
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
</div>
<Paragraph>Now follow this guide:</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/manual/astro"
variant="primary/medium"
TrailingIcon="external-link"
>
Manual installation guide
</LinkButton>
<div className="flex items-start justify-start gap-2"></div>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very simple
example Job in <InlineCode variant="extra-small">example.ts</InlineCode> to help you
get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Astro app" />
<StepContentContainer>
@@ -1,21 +1,120 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
import { Feedback } from "~/components/Feedback";
import { PageGradient } from "~/components/PageGradient";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { Handle } from "~/utils/handle";
import { trimTrailingSlash } from "~/utils/pathBuilder";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Express" />,
};
export default function Page() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
return (
<FrameworkComingSoon
frameworkName="Express"
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/451"
githubIssueNumber={451}
>
<ExpressLogo className="w-56" />
</FrameworkComingSoon>
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<ExpressLogo className="w-64" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
</Header1>
<div className="flex items-center gap-2">
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<div>
<Callout
variant={"info"}
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
className="mb-8"
>
Trigger.dev has full support for serverless. We will be adding support for long-running
servers soon.
</Callout>
<div>
<StepNumber
stepNumber="1"
title="Manually set up Trigger.dev in your existing Express project"
/>
<StepContentContainer className="flex flex-col gap-2">
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
<div className="mb-2 flex w-full items-center justify-between">
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
</div>
<Paragraph>Now follow this guide:</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/manual/express"
variant="primary/medium"
TrailingIcon="external-link"
>
Manual installation guide
</LinkButton>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Express app" />
<StepContentContainer>
<RunDevCommand />
<Callout variant="info">
You may be using the `start` script instead, in which case substitute `dev` in the
above commands.
</Callout>
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</div>
</div>
</div>
</PageGradient>
);
}
@@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
type SelectionChoices = "use-existing-project" | "create-new-next-app";
@@ -48,6 +49,9 @@ export default function SetupNextjs() {
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<NextjsLogo className="w-56" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
@@ -27,6 +27,7 @@ import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { Badge } from "~/components/primitives/Badge";
import { RemixLogo } from "~/assets/logos/RemixLogo";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Remix" />,
@@ -43,6 +44,9 @@ export default function SetUpRemix() {
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<RemixLogo className="w-64" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (prisma) => {
const existingTask = await prisma.task.findUnique({
const task = await this.#prismaClient.$transaction(async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
},
@@ -129,35 +129,31 @@ export class CompleteRunTaskService {
return existingTask;
}
const task = await $transaction(prisma, async (tx) => {
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
},
});
}
return await tx.task.update({
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id,
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
},
});
});
}
return task;
return await tx.task.update({
where: {
id,
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
},
});
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -86,8 +86,8 @@ export class FailRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (prisma) => {
const existingTask = await prisma.task.findUnique({
const task = await this.#prismaClient.$transaction(async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
},
@@ -129,35 +129,31 @@ export class FailRunTaskService {
return existingTask;
}
const task = await $transaction(prisma, async (tx) => {
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "ERRORED",
error: formatError(taskBody.error),
},
});
}
return await prisma.task.update({
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id,
id: existingTask.attempts[0].id,
},
data: {
status: "ERRORED",
output: taskBody.error ?? undefined,
completedAt: new Date(),
},
include: {
attempts: true,
error: formatError(taskBody.error),
},
});
});
}
return task;
return await tx.task.update({
where: {
id,
},
data: {
status: "ERRORED",
output: taskBody.error ?? undefined,
completedAt: new Date(),
},
include: {
attempts: true,
},
});
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
@@ -34,77 +34,55 @@ export class IngestSendEvent {
try {
const deliverAt = this.#calculateDeliverAt(options);
return await $transaction(
this.#prismaClient,
async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
},
},
create: {
return await $transaction(this.#prismaClient, async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: options.accountId,
},
update: {},
})
: undefined;
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: options.accountId,
},
update: {},
})
: undefined;
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
environment: {
connect: {
id: environment.id,
},
},
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccount: externalAccount
? {
connect: {
id: externalAccount.id,
},
}
: {},
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organizationId: environment.organizationId,
projectId: environment.projectId,
environmentId: environment.id,
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccountId: externalAccount ? externalAccount.id : undefined,
},
});
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
});
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
);
}
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
);
}
return eventLog;
},
{ rethrowPrismaErrors: true }
);
return eventLog;
});
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
@@ -42,29 +42,32 @@ export class CreateRunService {
return await $transaction(this.#prismaClient, async (tx) => {
// Get the current max number for the given jobId
const currentMaxNumber = await tx.jobRun.aggregate({
const latestJob = await tx.jobRun.findFirst({
where: { jobId: job.id },
_max: { number: true },
orderBy: { id: "desc" },
select: {
number: true,
},
});
// Increment the number for the new execution
const newNumber = (currentMaxNumber._max.number ?? 0) + 1;
const newNumber = (latestJob?.number ?? 0) + 1;
// Create the new execution with the incremented number
const run = await tx.jobRun.create({
data: {
number: newNumber,
preprocess: version.preprocessRuns,
job: { connect: { id: job.id } },
version: { connect: { id: version.id } },
event: { connect: { id: eventId } },
environment: { connect: { id: environment.id } },
organization: { connect: { id: environment.organizationId } },
project: { connect: { id: environment.projectId } },
endpoint: { connect: { id: endpoint.id } },
queue: { connect: { id: jobQueue.id } },
externalAccount: eventRecord.externalAccountId
? { connect: { id: eventRecord.externalAccountId } }
jobId: job.id,
versionId: version.id,
eventId: eventId,
environmentId: environment.id,
organizationId: environment.organizationId,
projectId: environment.projectId,
endpointId: endpoint.id,
queueId: jobQueue.id,
externalAccountId: eventRecord.externalAccountId
? eventRecord.externalAccountId
: undefined,
isTest: eventRecord.isTest,
},
@@ -153,6 +153,29 @@ export class PerformRunExecutionV2Service {
return;
}
try {
if (
typeof process.env.BLOCKED_ORGS === "string" &&
process.env.BLOCKED_ORGS.includes(run.organizationId)
) {
logger.debug("Skipping execution for blocked org", {
orgId: run.organizationId,
});
await this.#prismaClient.jobRun.update({
where: {
id: run.id,
},
data: {
status: "CANCELED",
completedAt: new Date(),
},
});
return;
}
} catch (e) {}
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = eventRecordToApiJson(run.event);
+2 -1
View File
@@ -161,7 +161,8 @@ function getWorkerQueue() {
tasks: {
"events.invokeDispatcher": {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
maxAttempts: 6,
queueName: (payload) => `dispatcher:${payload.id}`, // use a queue for a dispatcher so runs are created sequentially
handler: async (payload, job) => {
const service = new InvokeDispatcherService();
+71
View File
@@ -0,0 +1,71 @@
// Redacts the given object based on the given paths
// Example:
// const redactor = new Redactor(["data.object.balance_transaction"]);
// redactor.redact({
// data: {
// object: {
// balance_transaction: "txn_1NYWgTI0XSgju2urW3aXpinM",
// },
// },
// });
// Returns:
// {
// data: {
// object: {
// balance_transaction: "[REDACTED]",
// },
// },
// }
// Does not currenly support arrays
export class Redactor {
constructor(private paths: string[]) {}
public redact(subject: unknown): unknown {
if (!Array.isArray(this.paths)) {
return subject;
}
if (this.paths.length === 0) {
return subject;
}
const clonedSubject = JSON.parse(JSON.stringify(subject));
return this.redactPathsRecursive(clonedSubject, this.paths);
}
private redactPathsRecursive(subject: any, paths: string[]): any {
for (let path of paths) {
let parts = path.split(".");
let curSubject = subject;
// Make sure curSubject is an object
if (typeof curSubject !== "object") {
break;
}
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (Object.prototype.hasOwnProperty.call(curSubject, part) === false) {
// Path is not found in object
break;
}
if (i === parts.length - 1) {
// We're at the end of our path and have a string, redact it
curSubject[part] = "[REDACTED]";
} else if (part in curSubject && typeof curSubject[part] === "object") {
// More paths to follow, continue down the path
curSubject = curSubject[part];
} else {
// Path is not found in object or doesn't point to a string
break;
}
}
}
return subject;
}
}
+2 -2
View File
@@ -61,8 +61,8 @@
"@remix-run/server-runtime": "1.19.2-pre.0",
"@team-plain/typescript-sdk": "^2.2.0",
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@uiw/react-codemirror": "^4.19.5",
"class-variance-authority": "^0.5.2",
@@ -73,7 +73,6 @@
"cuid": "^2.1.8",
"emails": "workspace:*",
"express": "^4.18.1",
"fast-redact": "^3.1.2",
"framer-motion": "^10.12.11",
"graphile-worker": "^0.13.0",
"highlight.run": "^7.3.4",
@@ -96,6 +95,7 @@
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^3.4.7",
"react-use": "^17.4.0",
"recharts": "^2.8.0",
"remix-auth": "^3.2.2",
"remix-auth-email-link": "^1.4.2",
"remix-auth-github": "^1.1.1",
+67
View File
@@ -3,6 +3,7 @@
import { integrationCatalog } from "../app/services/externalApis/integrationCatalog.server";
import { seedCloud } from "./seedCloud";
import { prisma } from "../app/db.server";
import { createEnvironment } from "~/models/organization.server";
async function seedIntegrationAuthMethods() {
for (const [_, integration] of Object.entries(integrationCatalog.getIntegrations())) {
@@ -67,12 +68,78 @@ async function seedIntegrationAuthMethods() {
}
}
async function runDataMigrations() {
await runStagingEnvironmentMigration();
}
async function runStagingEnvironmentMigration() {
try {
await prisma.$transaction(async (tx) => {
const existingDataMigration = await tx.dataMigration.findUnique({
where: {
name: "2023-09-27-AddStagingEnvironments",
},
});
if (existingDataMigration) {
return;
}
await tx.dataMigration.create({
data: {
name: "2023-09-27-AddStagingEnvironments",
},
});
console.log("Running data migration 2023-09-27-AddStagingEnvironments");
const projectsWithoutStagingEnvironments = await tx.project.findMany({
where: {
environments: {
none: {
type: "STAGING",
},
},
},
include: {
organization: true,
},
});
for (const project of projectsWithoutStagingEnvironments) {
try {
console.log(
`Creating staging environment for project ${project.slug} on org ${project.organization.slug}`
);
await createEnvironment(project.organization, project, "STAGING", undefined, tx);
} catch (error) {
console.error(error);
}
}
await tx.dataMigration.update({
where: {
name: "2023-09-27-AddStagingEnvironments",
},
data: {
completedAt: new Date(),
},
});
});
} catch (error) {
console.error(error);
}
}
async function seed() {
await seedIntegrationAuthMethods();
if (process.env.NODE_ENV === "development" && process.env.SEED_CLOUD === "enabled") {
await seedCloud(prisma);
}
await runDataMigrations();
}
seed()
+3
View File
@@ -0,0 +1,3 @@
<Card title="React hooks" icon="fishing-rod" href="/documentation/guides/react-hooks">
Show the live status of Job Runs in your React app
</Card>
+193 -1
View File
@@ -1 +1,193 @@
We're in the process of building support for the Express framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues).
## Installing Required Packages
Start by installing the necessary packages in your Express.js project directory. You can use npm, pnpm, or yarn as your package manager.
<CodeGroup>
```bash npm
npm install @trigger.dev/sdk @trigger.dev/express
```
```bash pnpm
pnpm install @trigger.dev/sdk @trigger.dev/express
```
```bash yarn
yarn add @trigger.dev/sdk @trigger.dev/express
```
</CodeGroup>
<br />
<Note>Ensure that you execute this command within a Express project.</Note>
## Obtaining the Development Server API Key
To locate your development Server API key, login to the [Trigger.dev
dashboard](https://cloud.trigger.dev) and select the Project you want to
connect to. Then click on the Environments & API Keys tab in the left menu.
You can copy your development Server API Key from the field at the top of this page.
(Your development key will start with `tr_dev_`).
## Adding Environment Variables
Create a `.env` file at the root of your project and include your Trigger API key and URL like this:
```bash
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
```
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
## Configuring the Trigger Client
Create a file for your Trigger client, in this case we create it at `<root>/trigger.(ts/js)`
```ts trigger.(ts/js)
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "my-app",
apiKey: process.env.TRIGGER_API_KEY!,
apiUrl: process.env.TRIGGER_API_URL!,
});
```
Replace **"my-app"** with an appropriate identifier for your project.
## Adding the API endpoint
There are a few different options depending on how your Express project is configured.
- App middleware
- Entire app for Trigger.dev (only relevant if it's the only thing your project is for)
Select the appropriate code example from below:
<CodeGroup>
```typescript app middleware
//import the client from the other file
import { client } from "./trigger";
import { createMiddleware } from "@trigger.dev/express";
//import your job files
import "./jobs/example";
//..your existing Express code
const app: Express = express();
//add the middleware
app.use(createMiddleware(client));
//..the rest of your Express code
```
```typescript entire app
//if the entire app is just for Trigger.dev
import { client } from "./trigger";
import { createExpressServer } from "@trigger.dev/express";
//import your job files
import "./jobs/example";
//this creates an app
createExpressServer(client);
```
</CodeGroup>
## Creating the Example Job
Create a Job file. In this case created `<root>/jobs/example.(ts/js)`
```typescript jobs/example.(ts/js)
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "../trigger";
// your first job
client.defineJob({
id: "example-job",
name: "Example Job",
version: "0.0.1",
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
await io.logger.info("Hello world!", { payload });
return {
message: "Hello world!",
};
},
});
```
## Adding Configuration to `package.json`
Inside the `package.json` file, add the following configuration under the root object:
```json
"trigger.dev": {
"endpointId": "my-app"
}
```
Replace **"my-app"** with the appropriate identifier you used in the trigger.js configuration file.
## Running
### Run your Express app
Run your Express app locally, like you normally would. For example:
<CodeGroup>
```bash npm
npm run dev
```
```bash pnpm
pnpm run dev
```
```bash yarn
yarn run dev
```
</CodeGroup>
<Note>You might use `npm run start` instead of dev</Note>
### Run the CLI 'dev' command
In a **_separate terminal window or tab_** run:
<CodeGroup>
```bash npm
npx @trigger.dev/cli@latest dev
```
```bash pnpm
pnpm dlx @trigger.dev/cli@latest dev
```
```bash yarn
yarn dlx @trigger.dev/cli@latest dev
```
</CodeGroup>
<br />
<Note>
You can optionally pass the port if you're not running on 3000 by adding
`--port 3001` to the end
</Note>
<Note>
You can optionally pass the hostname if you're not running on localhost by adding
`--hostname <host>`. Example, in case your Express is running on 0.0.0.0: `--hostname 0.0.0.0`.
</Note>
@@ -27,6 +27,10 @@ The `DEV` environment should only be used for local development. It's where you
<Snippet file="scheduled-dev-warning.mdx" />
### Staging
The `STAGING` environment is useful for testing your Jobs against your staging server, if you have one. STAGING works identically to PROD.
### Production
The `PROD` environment is where your Jobs will run in production. It's where you can run your Jobs against real data.
+3 -3
View File
@@ -22,15 +22,15 @@ To begin, install the necessary packages in your Next.js project directory. You
<CodeGroup>
```bash npm
npm i @trigger.dev/sdk @trigger-dev/nextjs
npm i @trigger.dev/sdk @trigger.dev/nextjs
```
```bash pnpm
pnpm install @trigger.dev/sdk @trigger-dev/nextjs
pnpm install @trigger.dev/sdk @trigger.dev/nextjs
```
```bash yarn
yarn add @trigger.dev/sdk @trigger-dev/nextjs
yarn add @trigger.dev/sdk @trigger.dev/nextjs
```
</CodeGroup>
+94 -22
View File
@@ -48,38 +48,110 @@ This guide assumes that your project is already setup and you have a Job running
</Accordion>
</Step>
<Step title="Add the env var to your project">
Add the `NEXT_PUBLIC_TRIGGER_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
<Step title="Setting up environment variables">
<Tabs>
<Tab title="Next.js">
Add the `NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
```sh .env.local
#...
TRIGGER_API_KEY=[your_private_api_key]
NEXT_PUBLIC_TRIGGER_API_KEY=[your_public_api_key]
#...
```
```sh .env.local
#...
TRIGGER_API_KEY=[your_private_api_key]
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
#...
```
Your private API key should already be in there.
Your private API key should already be in there.
`NEXT_PUBLIC_` is a special prefix that exposes the environment variable to your users' web browsers.
</Tab>
<Tab title="Remix/React">
Add the `TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
```sh .env
#...
TRIGGER_API_KEY=[your_private_api_key]
TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
#...
```
You will need to pass this value from the server to the client. We recommend you do this in your Root loader.
</Tab>
</Tabs>
</Step>
<Step title="Add the <TriggerProvider> component">
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
Generally you'll want to add this to the root of your app, so that it's available everywhere. However, you can add it lower in the hierarchy but it must be above any of the hooks.
```tsx app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_API_KEY!}>
{children}
</TriggerProvider>
</body>
</html>
);
}
```
<Tabs>
<Tab title="Next.js">
```tsx app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY!}>
{children}
</TriggerProvider>
</body>
</html>
);
}
```
</Tab>
<Tab title="Remix">
```tsx app/root.tsx
//return the public key env var from the loader so it's available in the browser
export const loader = async ({ request }: LoaderArgs) => {
//...other code
const triggerPublicApiKey = env.TRIGGER_PUBLIC_API_KEY!;
return json({
//...other data
triggerPublicApiKey
});
}
//Your default export, i.e. the page component
export default function App() {
const {
//...other data
triggerPublicApiKey
} = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body className={inter.className}>
{/* wrap your outlet in this */}
<TriggerProvider publicApiKey={triggerPublicApiKey}>
<Outlet />
</TriggerProvider>
<ScrollRestoration />
<ExternalScripts />
<Scripts />
<LiveReload />
</body>
</html>
);
}
```
</Tab>
</Tabs>
</Step>
</Steps>
+78 -1
View File
@@ -4,4 +4,81 @@ sidebarTitle: "Astro"
description: "Start creating Jobs in 5 minutes in your Astro project."
---
<Snippet file="manual-setup-astro.mdx" />
This quick start guide will get you up and running with Trigger.dev.
<Accordion title="Need to create a new Astro project to add Trigger.dev to?">
No problem, create a blank project by running the `create-astro` command in your terminal then continue with this quickstart guide as normal:
```bash
npx create-astro@latest
```
</Accordion>
<Steps titleSize="h3">
<Snippet file="quickstart-setup-steps.mdx" />
<Step title="Run the CLI `dev` command">
<Snippet file="quickstart-cli-dev.mdx" />
<AccordionGroup>
<Accordion title="Advanced: Run your Astro server together with the CLI">
You can modify your `package.json` to run both the Astro server and the CLI `dev` command together.
1. Install the `concurrently` package:
<CodeGroup>
```bash npm
npm install concurrently --save-dev
```
```bash pnpm
pnpm install concurrently --save-dev
```
```bash yarn
yarn add concurrently --dev
```
</CodeGroup>
2. Modify your `package.json` file's `dev` script.
```json package.json
//...
"scripts": {
"dev": "concurrently --kill-others npm:dev:*",
//your normal astro dev command would go here
"dev:astro": "astro dev",
"dev:trigger": "npx @trigger.dev/cli dev",
//...
}
//...
```
</Accordion>
</AccordionGroup>
</Step>
<Step title="Your first job">
The CLI init command created a simple Job for you. There will be a new file `src/jobs/example.(ts/js)`.
In there is this Job:
<Snippet file="quickstart-example-job.mdx" />
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
![Your first Job](/images/first-job.png)
</Step>
<Snippet file="quickstart-running-your-job.mdx" />
</Steps>
<Snippet file="quickstart-whats-next.mdx" />
@@ -87,3 +87,6 @@ If you navigate to your Trigger.dev project you will see this Job in the "Jobs"
</Steps>
<Snippet file="quickstart-whats-next.mdx" />
<CardGroup cols={2}>
<Snippet file="card-react-hooks.mdx" />
</CardGroup>
+4 -1
View File
@@ -67,7 +67,7 @@ yarn add concurrently --dev
<Step title="Your first job">
The CLI init command created a simple Job for you. There will be a new file either `app/jobs/example.server.(ts/js)`.
The CLI init command created a simple Job for you. There will be a new file `app/jobs/example.server.(ts/js)`.
In there is this Job:
@@ -84,3 +84,6 @@ If you navigate to your Trigger.dev project you will see this Job in the "Jobs"
</Steps>
<Snippet file="quickstart-whats-next.mdx" />
<CardGroup cols={2}>
<Snippet file="card-react-hooks.mdx" />
</CardGroup>
+1
View File
@@ -276,6 +276,7 @@
"pages": [
"sdk/triggerclient/instancemethods/sendevent",
"sdk/triggerclient/instancemethods/getevent",
"sdk/triggerclient/instancemethods/cancel-event",
"sdk/triggerclient/instancemethods/getruns",
"sdk/triggerclient/instancemethods/getrun",
"sdk/triggerclient/instancemethods/define-job",
+1 -1
View File
@@ -87,7 +87,7 @@ description: "Use this method to register a new schedule with the DynamicSchedul
An object containing options about the interval.
<Expandable title="options" defaultOpen>
<ResponseField name="seconds" type="number" required>
The number of seconds for the interval. Min = 60, Max = 86400 (1 day)
The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days)
</ResponseField>
</Expandable>
</ResponseField>
+2 -2
View File
@@ -13,7 +13,7 @@ Intervals are set with a number of seconds. There are some important considerati
- The Job will first run the specified number of seconds after it has first connected to an [Environment](/documentation/concepts/environments-endpoints). This will happen when you first [deploy](/documentation/guides/deployment) that Job.
- The minimum interval is 60 seconds (any input less than this it will default to 60).
- The maximum interval is 86400 seconds (24 hours), if you pass more than this it will trigger every 24 hours.
- The maximum interval is 2_592_000 seconds (30 days), if you pass more than this it will trigger every 30 days.
If you wish to Run a Job at an exact time or less frequently than once pr day you should use a [cronTrigger()](/sdk/crontrigger) instead.
@@ -22,7 +22,7 @@ If you wish to Run a Job at an exact time or less frequently than once pr day yo
<ResponseField name="options" type="object" required>
<Expandable title="options" defaultOpen>
<ResponseField name="seconds" type="number" required>
The number of seconds for the interval. Min = 60, Max = 86400 (1 day)
The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days)
</ResponseField>
</Expandable>
</ResponseField>
+2 -2
View File
@@ -23,7 +23,7 @@ description: "`io.registerInterval()` allows you to register a [DynamicSchedule]
<Expandable title="options" defaultOpen>
<ResponseField name="seconds" type="number" required>
The number of seconds for the interval. Min = 60, Max = 86400 (1 day)
The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days)
</ResponseField>
</Expandable>
@@ -51,7 +51,7 @@ A Promise that resolves to an object with the following fields:
An object containing options about the interval.
<Expandable title="options" defaultOpen>
<ResponseField name="seconds" type="number" required>
The number of seconds for the interval. Min = 60, Max = 86400 (1 day)
The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days)
</ResponseField>
</Expandable>
</ResponseField>
+17 -3
View File
@@ -82,9 +82,12 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged.
</Expandable>
</ResponseField>
<ResponseField name="params" type="any">
The input params to the Task, will be displayed in the logs.
</ResponseField>
{" "}
<ResponseField name="params" type="any">
The input params to the Task, will be displayed in the logs.
</ResponseField>
<ResponseField name="style" type="object">
The style of the log entry.
@@ -98,6 +101,17 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged.
</Expandable>
</ResponseField>
<ResponseField name="redact" type="RedactOptions">
An optional object that specifies which fields to redact from the logs. This is useful for sensitive data like API keys.
<Expandable title="redact" defaultOpen>
<ResponseField name="paths" type="string[]">
An array of paths to redact. A path is a dot separated string, e.g. `user.email`. Currently does not support wildcards.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
@@ -0,0 +1,31 @@
---
title: "TriggerClient: cancelEvent() instance method"
sidebarTitle: "cancelEvent()"
description: "The `cancelEvent()` instance method will cancel an event that is scheduled to be delivered in the future."
---
If you've scheduled an event to be delivered in the future, you can cancel it using the `cancelEvent()` instance method, passing in the ID of the event you want to cancel. This will prevent any jobs listening for that event from being triggered.
```ts
// Sending an event that will be delivered in 24 hours
const event = await client.sendEvent(
{
id: "event_12345",
name: "my.event",
payload: {
foo: "bar",
},
},
{
deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // deliver in 24 hours
}
);
// Sometime later, cancel the event by ID
await client.cancelEvent(event.id);
```
<Note>
Cancelling an event after it has already triggered a job run does not cancel the job run.
Cancelling events only prevent the event from triggering future job runs.
</Note>
+9
View File
@@ -12,6 +12,11 @@ export const client = new TriggerClient({
});
```
<Warning>
The `TriggerClient` should only ever be used in a server-side environment. It is not safe to use
in a browser environment because it exposes your API Key.
</Warning>
## Constructor
### [TriggerClient()](/sdk/triggerclient/constructor)
@@ -36,6 +41,10 @@ You can call this function from anywhere in your code to send an event. The othe
The `getEvent()` method gets the event details for a given eventId.
#### [cancelEvent()](/sdk/triggerclient/instancemethods/cancel-event)
The `cancelEvent()` method cancels an event that is scheduled to be delivered in the future.
#### [getRuns()](/sdk/triggerclient/instancemethods/getruns)
The `getRuns()` method gets runs for a Job.
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/airtable
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"@trigger.dev/sdk": "workspace:^2.1.7",
"airtable": "^0.12.1",
"zod": "3.21.4"
},
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/github
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "2.1.4",
"version": "2.1.7",
"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.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.7",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"octokit": "^2.0.14",
"zod": "3.21.4"
},
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/linear
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"@trigger.dev/sdk": "workspace:^2.1.7",
"zod": "3.21.4"
},
"engines": {
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/slack
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "2.1.4",
"version": "2.1.7",
"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.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.4"
"@trigger.dev/sdk": "workspace:^2.1.7",
"@trigger.dev/integration-kit": "workspace:^2.1.7"
},
"engines": {
"node": ">=16.8.0"
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/plain
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "2.1.4",
"version": "2.1.7",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"@trigger.dev/sdk": "workspace:^2.1.7",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/resend
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "2.1.4",
"version": "2.1.7",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"@trigger.dev/sdk": "workspace:^2.1.7",
"resend": "^1.0.0"
},
"engines": {
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/sendgrid
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.4"
"@trigger.dev/sdk": "workspace:^2.1.7",
"@trigger.dev/integration-kit": "workspace:^2.1.7"
},
"engines": {
"node": ">=16.8.0"
+18
View File
@@ -1,5 +1,23 @@
# @trigger.dev/slack
## 2.1.7
### Patch Changes
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "2.1.4",
"version": "2.1.7",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.7",
"zod": "3.21.4"
},
"engines": {
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/stripe
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"@trigger.dev/sdk": "workspace:^2.1.7",
"stripe": "^12.14.0",
"zod": "3.21.4"
},
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/supabase
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"@trigger.dev/sdk": "workspace:^2.1.7",
"supabase-management-js": "^0.1.4",
"zod": "3.21.4"
},
+21
View File
@@ -1,5 +1,26 @@
# @trigger.dev/typeform
## 2.1.7
### Patch Changes
- @trigger.dev/integration-kit@2.1.7
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/integration-kit@2.1.6
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/integration-kit@2.1.5
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "2.1.4",
"version": "2.1.7",
"description": "The official Typeform integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@typeform/api-client": "^1.8.0",
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/integration-kit": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.7",
"@trigger.dev/integration-kit": "workspace:^2.1.7",
"zod": "3.21.4"
},
"engines": {
+18
View File
@@ -1,5 +1,23 @@
# @trigger.dev/astro
## 2.1.7
### Patch Changes
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/astro",
"description": "An Astro-native integration for Trigger.dev background jobs platform",
"version": "2.1.4",
"version": "2.1.7",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
@@ -20,7 +20,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.1.4"
"@trigger.dev/sdk": "workspace:^2.1.7"
},
"devDependencies": {
"astro": "^3.0.12",
+18
View File
@@ -1,5 +1,23 @@
# create-trigger
## 2.1.7
### Patch Changes
- 7760e094: Improved CLI init Next.js middleware detection
## 2.1.6
### Patch Changes
- 2397fcb6: Added Express support to the CLI
## 2.1.5
### Patch Changes
- 4578f6bd: Added Astro automatic installation
## 2.1.4
### Patch Changes
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/cli",
"version": "2.1.4",
"version": "2.1.7",
"description": "The Trigger.dev CLI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -57,6 +57,7 @@
},
"dependencies": {
"@types/degit": "^2.8.3",
"boxen": "^7.1.1",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
"commander": "^9.4.1",
+14 -5
View File
@@ -13,6 +13,7 @@ import {
getEnvFilename,
setApiKeyEnvironmentVariable,
setApiUrlEnvironmentVariable,
setPublicApiKeyEnvironmentVariable,
} from "../utils/env";
import { readJSONFile } from "../utils/fileSystem";
import { PackageManager, getUserPackageManager } from "../utils/getUserPkgManager";
@@ -114,6 +115,12 @@ export const initCommand = async (options: InitCommandOptions) => {
}
await setApiKeyEnvironmentVariable(resolvedPath, envName, resolvedOptions.apiKey);
await setApiUrlEnvironmentVariable(resolvedPath, envName, resolvedOptions.apiUrl);
await setPublicApiKeyEnvironmentVariable(
resolvedPath,
envName,
framework.publicKeyEnvName,
authorizedKey.pkApiKey
);
const installOptions = {
typescript: isTypescriptProject,
@@ -129,18 +136,20 @@ export const initCommand = async (options: InitCommandOptions) => {
await addConfigurationToPackageJson(resolvedPath, resolvedOptions);
await printNextSteps(resolvedOptions, authorizedKey, packageManager, framework);
const projectUrl = `${resolvedOptions.triggerUrl}/orgs/${authorizedKey.organization.slug}/projects/${authorizedKey.project.slug}`;
if (framework.printInstallationComplete) {
await framework.printInstallationComplete(projectUrl);
} else {
await printNextSteps(projectUrl, packageManager, framework);
}
telemetryClient.init.completed(resolvedOptions);
};
async function printNextSteps(
options: ResolvedOptions,
authorizedKey: WhoamiResponse,
projectUrl: string,
packageManager: PackageManager,
framework: Framework
) {
const projectUrl = `${options.triggerUrl}/orgs/${authorizedKey.organization.slug}/projects/${authorizedKey.project.slug}`;
logger.success(`✔ Successfully initialized Trigger.dev!`);
logger.info("Next steps:");
@@ -0,0 +1,81 @@
import mock from "mock-fs";
import { Astro } from ".";
import { getFramework } from "..";
import { pathExists } from "../../utils/fileSystem";
afterEach(() => {
mock.restore();
});
describe("Astro project detection", () => {
test("has dependency", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { astro: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("astro");
});
test("no dependency, has astro.config.js", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
"astro.config.js": "module.exports = {}",
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("astro");
});
test("no dependency, has astro.config.mjs", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
"astro.config.mjs": "module.exports = {}",
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("astro");
});
test("no dependency, no astro.config.*", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).not.toEqual("astro");
});
});
describe("install", () => {
test("javascript", async () => {
mock({
src: {
pages: {},
},
});
const astro = new Astro();
await astro.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" });
expect(await pathExists("src/trigger.js")).toEqual(true);
expect(await pathExists("src/pages/api/trigger.js")).toEqual(true);
expect(await pathExists("src/jobs/example.js")).toEqual(true);
expect(await pathExists("src/jobs/index.js")).toEqual(true);
});
test("typescript", async () => {
mock({
app: {
routes: {},
},
"tsconfig.json": JSON.stringify({}),
});
const astro = new Astro();
await astro.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" });
expect(await pathExists("src/trigger.ts")).toEqual(true);
expect(await pathExists("src/pages/api/trigger.ts")).toEqual(true);
expect(await pathExists("src/jobs/example.ts")).toEqual(true);
expect(await pathExists("src/jobs/index.ts")).toEqual(true);
});
});
+128
View File
@@ -0,0 +1,128 @@
import { Framework, ProjectInstallOptions } from "..";
import { InstallPackage } from "../../utils/addDependencies";
import { pathExists, someFileExists } from "../../utils/fileSystem";
import { PackageManager } from "../../utils/getUserPkgManager";
import pathModule from "path";
import { getPathAlias } from "../../utils/pathAlias";
import { createFileFromTemplate } from "../../utils/createFileFromTemplate";
import { templatesPath } from "../../paths";
import { logger } from "../../utils/logger";
import { readPackageJson } from "../../utils/readPackageJson";
import { standardWatchFilePaths } from "../watchConfig";
export class Astro implements Framework {
id = "astro";
name = "Astro";
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
const configFilenames = [
"astro.config.js",
"astro.config.mjs",
"astro.config.cjs",
"astro.config.ts",
];
//check for astro.config.mjs
const hasConfigFile = await someFileExists(path, configFilenames);
if (hasConfigFile) {
return true;
}
//check for the astro package
const packageJsonContent = await readPackageJson(path);
if (packageJsonContent?.dependencies?.astro) {
return true;
}
return false;
}
async dependencies(): Promise<InstallPackage[]> {
return [
{ name: "@trigger.dev/sdk", tag: "latest" },
{ name: "@trigger.dev/astro", tag: "latest" },
{ name: "@trigger.dev/react", tag: "latest" },
];
}
possibleEnvFilenames(): string[] {
return [".env", ".env.development"];
}
async install(path: string, { typescript, endpointSlug }: ProjectInstallOptions): Promise<void> {
const pathAlias = await getPathAlias({
projectPath: path,
isTypescriptProject: typescript,
extraDirectories: ["src"],
});
const templatesDir = pathModule.join(templatesPath(), "astro");
const srcFolder = pathModule.join(path, "src");
const fileExtension = typescript ? ".ts" : ".js";
//create src/pages/api/trigger.js
const apiRoutePath = pathModule.join(srcFolder, "pages", "api", `trigger${fileExtension}`);
const apiRouteResult = await createFileFromTemplate({
templatePath: pathModule.join(templatesDir, "apiRoute.js"),
replacements: {
routePathPrefix: pathAlias ? pathAlias + "/" : "../../",
},
outputPath: apiRoutePath,
});
if (!apiRouteResult.success) {
throw new Error("Failed to create API route file");
}
logger.success(`✔ Created API route at ${apiRoutePath}`);
//src/trigger.js
const triggerFilePath = pathModule.join(srcFolder, `trigger${fileExtension}`);
const triggerResult = await createFileFromTemplate({
templatePath: pathModule.join(templatesDir, "trigger.js"),
replacements: {
endpointSlug,
},
outputPath: triggerFilePath,
});
if (!triggerResult.success) {
throw new Error("Failed to create trigger file");
}
logger.success(`✔ Created Trigger client at ${triggerFilePath}`);
//src/jobs/example.js
const exampleJobFilePath = pathModule.join(srcFolder, "jobs", `example${fileExtension}`);
const exampleJobResult = await createFileFromTemplate({
templatePath: pathModule.join(templatesDir, "exampleJob.js"),
replacements: {
jobsPathPrefix: pathAlias ? pathAlias + "/" : "../",
},
outputPath: exampleJobFilePath,
});
if (!exampleJobResult.success) {
throw new Error("Failed to create example job file");
}
logger.success(`✔ Created example job at ${exampleJobFilePath}`);
//src/jobs/index.js
const jobsIndexFilePath = pathModule.join(srcFolder, "jobs", `index${fileExtension}`);
const jobsIndexResult = await createFileFromTemplate({
templatePath: pathModule.join(templatesDir, "jobsIndex.js"),
replacements: {
jobsPathPrefix: pathAlias ? pathAlias + "/" : "../",
},
outputPath: jobsIndexFilePath,
});
if (!jobsIndexResult.success) {
throw new Error("Failed to create jobs index file");
}
logger.success(`✔ Created jobs index at ${jobsIndexFilePath}`);
}
async postInstall(path: string, options: ProjectInstallOptions): Promise<void> {
logger.warn(
`⚠︎ Ensure your astro.config output is "server" or "hybrid":\nhttps://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project`
);
}
defaultHostnames = ["localhost", "[::]"];
defaultPorts = [4321, 4322, 4323, 4324];
watchFilePaths = standardWatchFilePaths;
watchIgnoreRegex = /(node_modules)/;
}
@@ -0,0 +1,28 @@
import mock from "mock-fs";
import { Express } from ".";
import { getFramework } from "..";
import { pathExists } from "../../utils/fileSystem";
afterEach(() => {
mock.restore();
});
describe("Express project detection", () => {
test("has dependency", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { express: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("express");
});
test("no dependency", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).not.toEqual("express");
});
});
@@ -0,0 +1,51 @@
import { Framework, ProjectInstallOptions } from "..";
import { InstallPackage } from "../../utils/addDependencies";
import { PackageManager } from "../../utils/getUserPkgManager";
import { logger } from "../../utils/logger";
import { readPackageJson } from "../../utils/readPackageJson";
import { standardWatchFilePaths } from "../watchConfig";
import boxen from "boxen";
export class Express implements Framework {
id = "express";
name = "Express";
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
//check for the express package
const packageJsonContent = await readPackageJson(path);
if (packageJsonContent?.dependencies?.express) {
return true;
}
return false;
}
async dependencies(): Promise<InstallPackage[]> {
return [
{ name: "@trigger.dev/sdk", tag: "latest" },
{ name: "@trigger.dev/express", tag: "latest" },
];
}
possibleEnvFilenames(): string[] {
return [".env"];
}
async install(path: string, { typescript, endpointSlug }: ProjectInstallOptions): Promise<void> {}
async postInstall(path: string, options: ProjectInstallOptions): Promise<void> {}
async printInstallationComplete(projectUrl: string): Promise<void> {
logger.info(
boxen(
"Automatic installation isn't currently supported for Express. \nFollow the steps in our manual installation guide: https://trigger.dev/docs/documentation/guides/manual/express",
{ padding: 1, margin: 1, borderStyle: "double", borderColor: "magenta" }
)
);
}
defaultHostnames = ["localhost", "[::]"];
defaultPorts = [3000, 8000, 80, 8080];
watchFilePaths = standardWatchFilePaths;
watchIgnoreRegex = /(node_modules)/;
}
+9 -1
View File
@@ -1,5 +1,7 @@
import { InstallPackage } from "../utils/addDependencies";
import { PackageManager } from "../utils/getUserPkgManager";
import { Astro } from "./astro";
import { Express } from "./express";
import { NextJs } from "./nextjs";
import { Remix } from "./remix";
@@ -25,12 +27,18 @@ export interface Framework {
/** Priority list of env filenames, e.g. ".env" */
possibleEnvFilenames(): string[];
/** Defaults to TRIGGER_PUBLIC_API_KEY */
publicKeyEnvName?: string;
/** Install the required files */
install(path: string, options: ProjectInstallOptions): Promise<void>;
/** You can check for middleware, add extra instructions, etc */
postInstall(path: string, options: ProjectInstallOptions): Promise<void>;
/** You can (optionally) override the initComplete messages */
printInstallationComplete?(projectUrl: string): Promise<void>;
/** Used by the dev command, if a hostname isn't passed in */
defaultHostnames: string[];
@@ -45,7 +53,7 @@ export interface Framework {
}
/** The order of these matters. The first one that matches the folder will be used, so stricter ones should be first. */
const frameworks: Framework[] = [new NextJs(), new Remix()];
const frameworks: Framework[] = [new NextJs(), new Remix(), new Astro(), new Express()];
export const getFramework = async (
path: string,
+38 -5
View File
@@ -4,13 +4,14 @@ import { Framework } from "..";
import { templatesPath } from "../../paths";
import { InstallPackage } from "../../utils/addDependencies";
import { createFileFromTemplate } from "../../utils/createFileFromTemplate";
import { pathExists } from "../../utils/fileSystem";
import { pathExists, someFileExists } from "../../utils/fileSystem";
import { PackageManager } from "../../utils/getUserPkgManager";
import { logger } from "../../utils/logger";
import { getPathAlias } from "../../utils/pathAlias";
import { readPackageJson } from "../../utils/readPackageJson";
import { detectMiddlewareUsage } from "./middleware";
import { standardWatchFilePaths } from "../watchConfig";
import { telemetryClient } from "../../telemetry/telemetry";
import { detectMiddlewareUsage } from "./middleware";
export class NextJs implements Framework {
id = "nextjs";
@@ -37,6 +38,8 @@ export class NextJs implements Framework {
return [".env.local", ".env"];
}
publicKeyEnvName = "NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY";
async install(
path: string,
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
@@ -65,7 +68,27 @@ export class NextJs implements Framework {
path: string,
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
): Promise<void> {
await detectMiddlewareUsage(path);
const result = await detectMiddlewareUsage(path, options.typescript);
if (result.hasMiddleware) {
switch (result.conflict) {
case "possible": {
logger.warn(
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${result.middlewarePath} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
break;
}
case "likely": {
logger.warn(
`🚨 It looks like there might be conflicting Next.js middleware in ${result.middlewarePath} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
break;
}
default:
break;
}
}
}
defaultHostnames = ["localhost"];
@@ -75,7 +98,14 @@ export class NextJs implements Framework {
}
async function detectNextConfigFile(path: string): Promise<boolean> {
return pathExists(pathModule.join(path, "next.config.js"));
const configFilenames = [
"next.config.js",
"next.config.mjs",
"next.config.cjs",
"next.config.ts",
];
return someFileExists(path, configFilenames);
}
export async function detectNextDependency(path: string): Promise<boolean> {
@@ -84,7 +114,10 @@ export async function detectNextDependency(path: string): Promise<boolean> {
return false;
}
return packageJsonContent.dependencies?.next !== undefined;
if (packageJsonContent.dependencies?.next !== undefined) return true;
if (packageJsonContent.devDependencies?.next !== undefined) return true;
return false;
}
export async function detectUseOfSrcDir(path: string): Promise<boolean> {
@@ -4,66 +4,87 @@ import pathModule from "path";
import { logger } from "../../utils/logger";
import { telemetryClient } from "../../telemetry/telemetry";
import { pathToRegexp } from "path-to-regexp";
import { detectUseOfSrcDir } from ".";
export async function detectMiddlewareUsage(path: string, usesSrcDir = false) {
const middlewarePath = pathModule.join(path, usesSrcDir ? "src" : "", "middleware.ts");
type Result =
| {
hasMiddleware: false;
}
| {
hasMiddleware: true;
conflict: "unlikely" | "possible" | "likely";
middlewarePath: string;
};
const middlewareExists = await pathExists(middlewarePath);
export async function detectMiddlewareUsage(path: string, typescript: boolean): Promise<Result> {
const usesSrcDir = await detectUseOfSrcDir(path);
const middlewarePath = pathModule.join(
path,
usesSrcDir ? "src" : "",
`middleware.${typescript ? "ts" : "js"}`
);
if (!middlewareExists) {
return;
try {
return await detectMiddleware(path, typescript, middlewarePath);
} catch (e) {
return {
hasMiddleware: true,
conflict: "possible",
middlewarePath: pathModule.relative(process.cwd(), middlewarePath),
};
}
}
async function detectMiddleware(
path: string,
typescript: boolean,
middlewarePath: string
): Promise<Result> {
const middlewareExists = await pathExists(middlewarePath);
if (!middlewareExists) {
return { hasMiddleware: false };
}
const middlewareRelativeFilePath = pathModule.relative(process.cwd(), middlewarePath);
const matcher = await getMiddlewareConfigMatcher(middlewarePath);
if (!matcher || matcher.length === 0) {
logger.warn(
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
process.cwd(),
middlewarePath
)} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
return;
return {
hasMiddleware: true,
conflict: "possible",
middlewarePath: middlewareRelativeFilePath,
};
}
if (matcher.length === 0) {
return;
return {
hasMiddleware: true,
conflict: "unlikely",
middlewarePath: middlewareRelativeFilePath,
};
}
if (typeof matcher === "string") {
const matcherRegex = pathToRegexp(matcher);
// Check to see if /api/trigger matches the regex, if it does, then we need to output a warning with a link to the docs to fix it
if (matcherRegex.test("/api/trigger")) {
logger.warn(
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
process.cwd(),
middlewarePath
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict_api_trigger", { projectPath: path });
}
} else if (Array.isArray(matcher) && matcher.every((m) => typeof m === "string")) {
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
logger.warn(
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
process.cwd(),
middlewarePath
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
}
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
return {
hasMiddleware: true,
conflict: "likely",
middlewarePath: middlewareRelativeFilePath,
};
}
return {
hasMiddleware: true,
conflict: "possible",
middlewarePath: middlewareRelativeFilePath,
};
}
async function getMiddlewareConfigMatcher(path: string): Promise<Array<string>> {
const fileContent = await fs.readFile(path, "utf-8");
const regex = /matcher:\s*(\[.*\]|".*")/s;
const regex = /matcher:\s*(\[.*\]|["'].*["'])/g;
let match = regex.exec(fileContent);
if (!match) {
@@ -2,6 +2,7 @@ import mock from "mock-fs";
import { NextJs, detectPagesOrAppDir, detectUseOfSrcDir } from ".";
import { getFramework } from "..";
import { pathExists } from "../../utils/fileSystem";
import { detectMiddlewareUsage } from "./middleware";
afterEach(() => {
mock.restore();
@@ -17,6 +18,15 @@ describe("Next project detection", () => {
expect(framework?.id).toEqual("nextjs");
});
test("has dev dependency", async () => {
mock({
"package.json": JSON.stringify({ devDependencies: { next: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("nextjs");
});
test("no dependency, has next.config.js", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
@@ -27,6 +37,16 @@ describe("Next project detection", () => {
expect(framework?.id).toEqual("nextjs");
});
test("no dependency, has next.config.mjs", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
"next.config.mjs": "module.exports = {}",
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("nextjs");
});
test("no dependency, no next.config.js", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
@@ -238,3 +258,130 @@ describe("app install", () => {
expect(await pathExists("jobs/examples.ts")).toEqual(true);
});
});
describe("Next middleware detection", () => {
test("no middleware", async () => {
mock({});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(false);
});
test("Basic middleware", async () => {
mock({
"middleware.js": `import { NextResponse } from 'next/server'
export function middleware(request) {
return NextResponse.redirect(new URL('/home', request.url))
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
}`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("Wildcard that throws middleware", async () => {
mock({
"middleware.js": `export const config = {
matcher: "*",
}`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("Array middleware", async () => {
mock({
"middleware.js": `export const config = {
matcher: ['/about/:path*', "/dashboard/:path*"],
}`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("With dashes middleware", async () => {
mock({
"middleware.js": `export const config = {
matcher: ["/configurations-test/:path*", "/projects/:path*"],
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("Likely double quoted string", async () => {
mock({
"middleware.js": `export const config = {
matcher: "/(.*)",
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
test("Likely single quoted string", async () => {
mock({
"middleware.js": `export const config = {
matcher: '/(.*)',
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
test("Likely double quoted array", async () => {
mock({
"middleware.js": `export const config = {
matcher: ["/pages/", "/(.*)"],
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
test("Likely single quoted array", async () => {
mock({
"middleware.js": `export const config = {
matcher: ['/pages/', '/(.*)'],
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
});
@@ -0,0 +1,9 @@
import { createAstroRoute } from "@trigger.dev/astro";
//you may need to update this path to point at your trigger.ts file
import { client } from "${routePathPrefix}trigger";
//import your jobs
import "${routePathPrefix}jobs";
export const prerender = false;
export const { POST } = createAstroRoute(client);
@@ -0,0 +1,27 @@
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "${jobsPathPrefix}trigger";
// Your first job
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
client.defineJob({
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
id: "example-job",
name: "Example Job: a joke with a delay",
version: "0.0.1",
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
// This logs a message to the console
await io.logger.info("🧪 Example Job: a joke with a delay");
await io.logger.info("How do you comfort a JavaScript bug?");
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
await io.wait("Wait 5 seconds for the punchline...", 5);
await io.logger.info("You console it! 🤦");
await io.logger.info(
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
);
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
},
});
@@ -0,0 +1,3 @@
// export all your job files here
export * from "./example";
@@ -0,0 +1,7 @@
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "${endpointSlug}",
apiKey: import.meta.env.TRIGGER_API_KEY,
apiUrl: import.meta.env.TRIGGER_API_URL,
});
+16
View File
@@ -34,6 +34,22 @@ export async function setApiUrlEnvironmentVariable(dir: string, fileName: string
await setEnvironmentVariable(dir, fileName, "TRIGGER_API_URL", apiUrl, true);
}
export async function setPublicApiKeyEnvironmentVariable(
dir: string,
fileName: string,
varName: string | undefined,
publicApiKey: string
) {
await setEnvironmentVariable(
dir,
fileName,
varName ?? "TRIGGER_PUBLIC_API_KEY",
publicApiKey,
true,
renderApiKey
);
}
async function setEnvironmentVariable(
dir: string,
fileName: string,
+14
View File
@@ -20,6 +20,20 @@ export async function pathExists(path: string): Promise<boolean> {
}
}
export async function someFileExists(directory: string, filenames: string[]): Promise<boolean> {
for (let index = 0; index < filenames.length; index++) {
const filename = filenames[index];
if (!filename) continue;
const path = pathModule.join(directory, filename);
if (await pathExists(path)) {
return true;
}
}
return false;
}
export async function removeFile(path: string) {
await fsModule.unlink(path);
}
+1
View File
@@ -47,6 +47,7 @@ const WhoamiResponseSchema = z.object({
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
}),
pkApiKey: z.string(),
userId: z.string().optional(),
});
+6
View File
@@ -1,5 +1,11 @@
# internal-platform
## 2.1.7
## 2.1.6
## 2.1.5
## 2.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/core",
"version": "2.1.4",
"version": "2.1.7",
"description": "Core code used across the Trigger.dev SDK and platform",
"license": "MIT",
"main": "./dist/index.js",
+2 -2
View File
@@ -10,8 +10,8 @@ export const ScheduledPayloadSchema = z.object({
export type ScheduledPayload = z.infer<typeof ScheduledPayloadSchema>;
export const IntervalOptionsSchema = z.object({
/** The number of seconds for the interval. Min = 60, Max = 86400 (1 day) */
seconds: z.number().int().positive().min(60).max(86400),
/** The number of seconds for the interval. Min = 60, Max = 2_592_000 (30 days) */
seconds: z.number().int().positive().min(60).max(2_592_000),
});
/** Interval options */
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "DataMigration" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
CONSTRAINT "DataMigration_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DataMigration_name_key" ON "DataMigration"("name");
+9
View File
@@ -1091,3 +1091,12 @@ model ApiIntegrationVote {
@@unique([apiIdentifier, userId])
}
model DataMigration {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
}
+1 -1
View File
@@ -34,6 +34,6 @@
"typescript": "^4.9.4"
},
"engines": {
"node": ">=18.0.0 <19.0.0"
"node": ">=18.0.0"
}
}
+6
View File
@@ -1,5 +1,11 @@
# @trigger.dev/eslint-plugin
## 2.1.7
## 2.1.6
## 2.1.5
## 2.1.4
## 2.1.3
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/eslint-plugin",
"version": "2.1.4",
"version": "2.1.7",
"description": "ESLint plugin with trigger.dev best practices",
"keywords": [
"eslint",
+18
View File
@@ -1,5 +1,23 @@
# @trigger.dev/express
## 2.1.7
### Patch Changes
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/express",
"version": "2.1.4",
"version": "2.1.7",
"description": "Official Express adapter for Trigger.dev",
"license": "MIT",
"main": "./dist/index.js",
@@ -19,7 +19,7 @@
"./package.json": "./package.json"
},
"devDependencies": {
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.7",
"@trigger.dev/tsconfig": "workspace:*",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.13",
@@ -33,7 +33,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.1.4"
"@trigger.dev/sdk": "workspace:^2.1.7"
},
"dependencies": {
"@remix-run/web-fetch": "^4.3.5",
+6
View File
@@ -1,5 +1,11 @@
# @trigger.dev/integration-kit
## 2.1.7
## 2.1.6
## 2.1.5
## 2.1.4
## 2.1.3
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/integration-kit",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev Integration Kit has helpers to make creating integrations easier",
"license": "MIT",
"main": "./dist/index.js",
+18
View File
@@ -1,5 +1,23 @@
# @trigger.dev/nextjs
## 2.1.7
### Patch Changes
- @trigger.dev/sdk@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/sdk@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/sdk@2.1.5
## 2.1.4
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/nextjs",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev Next.js integration",
"license": "MIT",
"main": "./dist/index.js",
@@ -34,7 +34,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.1.4",
"@trigger.dev/sdk": "workspace:^2.1.7",
"next": ">=12.0.0 <14.0.0"
},
"dependencies": {
+18
View File
@@ -1,5 +1,23 @@
# @trigger.dev/react
## 2.1.7
### Patch Changes
- @trigger.dev/core@2.1.7
## 2.1.6
### Patch Changes
- @trigger.dev/core@2.1.6
## 2.1.5
### Patch Changes
- @trigger.dev/core@2.1.5
## 2.1.4
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/react",
"version": "2.1.4",
"version": "2.1.7",
"description": "Trigger.dev React SDK",
"license": "MIT",
"types": "dist/index.d.ts",
@@ -27,7 +27,7 @@
},
"dependencies": {
"@tanstack/react-query": "5.0.0-beta.2",
"@trigger.dev/core": "workspace:^2.1.4",
"@trigger.dev/core": "workspace:^2.1.7",
"debug": "^4.3.4",
"zod": "3.21.4"
},

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