Compare commits

...

17 Commits

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

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

* Use a strict undefined check instead

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

* Read a package json file

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

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

* Started work on the resource route

* Created ValidateCreateEndpointService and the EndpointValidateApi (name TBC)

* The TriggerClient responds with the id

* Tidied some stuff up

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

* Create cool-snakes-deny.md

---------

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

* Started adding TriggerClient telemetry

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

* add patch changeset

* Create silly-baboons-join.md

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

* rename examplesIndex to examplesIndexFileName

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

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-06 16:42:32 +01:00
67 changed files with 1515 additions and 548 deletions
+2
View File
@@ -20,6 +20,8 @@ const EnvironmentSchema = z.object({
.default(process.env.NODE_ENV),
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_URL: z.string().optional(),
HIGHLIGHT_PROJECT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
+99 -1
View File
@@ -1,4 +1,6 @@
import type {
CronItem,
CronItemOptions,
Job as GraphileJob,
Runner as GraphileRunner,
JobHelpers,
@@ -7,7 +9,7 @@ import type {
TaskList,
TaskSpec,
} from "graphile-worker";
import { run as graphileRun } from "graphile-worker";
import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
@@ -18,6 +20,13 @@ export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
}
const RawCronPayloadSchema = z.object({
_cron: z.object({
ts: z.coerce.date(),
backfilled: z.boolean(),
}),
});
const GraphileJobSchema = z.object({
id: z.coerce.string(),
queue_name: z.string().nullable(),
@@ -50,6 +59,19 @@ export type ZodTasks<TConsumerSchema extends MessageCatalogSchema> = {
};
};
type RecurringTaskPayload = {
ts: Date;
backfilled: boolean;
};
export type ZodRecurringTasks = {
[key: string]: {
pattern: string;
options?: CronItemOptions;
handler: (payload: RecurringTaskPayload, job: GraphileJob) => Promise<void>;
};
};
export type ZodWorkerEnqueueOptions = TaskSpec & {
tx?: PrismaClientOrTransaction;
};
@@ -59,6 +81,7 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
prisma: PrismaClient;
schema: TMessageCatalog;
tasks: ZodTasks<TMessageCatalog>;
recurringTasks?: ZodRecurringTasks;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
@@ -66,6 +89,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#prisma: PrismaClient;
#runnerOptions: RunnerOptions;
#tasks: ZodTasks<TMessageCatalog>;
#recurringTasks?: ZodRecurringTasks;
#runner?: GraphileRunner;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
@@ -73,6 +97,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#prisma = options.prisma;
this.#runnerOptions = options.runnerOptions;
this.#tasks = options.tasks;
this.#recurringTasks = options.recurringTasks;
}
public async initialize(): Promise<boolean> {
@@ -84,9 +109,12 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
runnerOptions: this.#runnerOptions,
});
const parsedCronItems = parseCronItems(this.#createCronItemsFromRecurringTasks());
this.#runner = await graphileRun({
...this.#runnerOptions,
taskList: this.#createTaskListFromTasks(),
parsedCronItems,
});
if (!this.#runner) {
@@ -192,9 +220,38 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
taskList[key] = task;
}
for (const [key] of Object.entries(this.#recurringTasks ?? {})) {
const task: Task = (payload, helpers) => {
return this.#handleRecurringTask(key, payload, helpers);
};
taskList[key] = task;
}
return taskList;
}
#createCronItemsFromRecurringTasks() {
const cronItems: CronItem[] = [];
if (!this.#recurringTasks) {
return cronItems;
}
for (const [key, task] of Object.entries(this.#recurringTasks)) {
const cronItem: CronItem = {
pattern: task.pattern,
identifier: key,
task: key,
options: task.options,
};
cronItems.push(cronItem);
}
return cronItems;
}
async #handleMessage<K extends keyof TMessageCatalog>(
typeName: K,
rawPayload: unknown,
@@ -226,4 +283,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
await task.handler(payload, job);
}
async #handleRecurringTask(
typeName: string,
rawPayload: unknown,
helpers: JobHelpers
): Promise<void> {
const job = helpers.job;
logger.debug("Received recurring task, calling handler", {
type: String(typeName),
payload: rawPayload,
job,
});
const recurringTask = this.#recurringTasks?.[typeName];
if (!recurringTask) {
throw new Error(`No recurring task for message type: ${String(typeName)}`);
}
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse recurring task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
try {
await recurringTask.handler(payload._cron, job);
} catch (error) {
logger.error("Failed to handle recurring task", {
error,
payload,
});
throw error;
}
}
}
@@ -0,0 +1,121 @@
import { conform, useForm, useInputEvent } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher } from "@remix-run/react";
import { InlineCode } from "~/components/code/InlineCode";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { FormError } from "~/components/primitives/FormError";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "~/components/primitives/Sheet";
import { TextLink } from "~/components/primitives/TextLink";
import { docsPath } from "~/utils/pathBuilder";
import { bodySchema } from "../resources.projects.$projectId.endpoint";
import { RuntimeEnvironment, RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { useRef, useState } from "react";
type FirstEndpointSheetProps = {
projectId: string;
environments: { id: string; type: RuntimeEnvironmentType }[];
};
export function FirstEndpointSheet({ projectId, environments }: FirstEndpointSheetProps) {
const setEndpointUrlFetcher = useFetcher();
const [form, { url, environmentId }] = useForm({
id: "new-endpoint-url",
lastSubmission: setEndpointUrlFetcher.data,
onValidate({ formData }) {
return parse(formData, { schema: bodySchema });
},
});
const loadingEndpointUrl = setEndpointUrlFetcher.state !== "idle";
return (
<Sheet>
<SheetTrigger>
<ButtonContent variant={"primary/medium"}>Add your first endpoint</ButtonContent>
</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
<div>
<Header1>Add your first endpoint</Header1>
<Paragraph variant="small">
We recommend you use{" "}
<TextLink href={docsPath("documentation/guides/cli")}>the CLI</TextLink> when working
in development.
</Paragraph>
</div>
</SheetHeader>
<SheetBody>
<setEndpointUrlFetcher.Form
method="post"
action={`/resources/projects/${projectId}/endpoint`}
{...form.props}
>
<InputGroup className="mb-4 max-w-none">
<Header2>Environment type</Header2>
<SelectGroup>
<Select name={"environmentId"} defaultValue={environments[0].id}>
<SelectTrigger size="secondary/small">
<SelectValue placeholder="Select environment" className="m-0 p-0" /> Environment
</SelectTrigger>
<SelectContent>
{environments.map((environment) => (
<SelectItem key={environment.id} value={environment.id}>
<EnvironmentLabel environment={environment} />
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<FormError id={environmentId.errorId}>{environmentId.error}</FormError>
</InputGroup>
<InputGroup className="max-w-none">
<Header2>Endpoint URL</Header2>
<div className="flex items-center">
<Input
className="rounded-r-none"
{...conform.input(url, { type: "url" })}
placeholder="URL for your Trigger API route"
/>
<Button
type="submit"
variant="primary/medium"
className="rounded-l-none"
disabled={loadingEndpointUrl}
LeadingIcon={loadingEndpointUrl ? "spinner-white" : undefined}
>
{loadingEndpointUrl ? "Saving" : "Save"}
</Button>
</div>
<FormError id={url.errorId}>{url.error}</FormError>
<FormError id={form.errorId}>{form.error}</FormError>
<Hint>
This is the URL of your Trigger API route, Typically this would be:{" "}
<InlineCode variant="extra-small">https://yourdomain.com/api/trigger</InlineCode>.
</Hint>
</InputGroup>
</setEndpointUrlFetcher.Form>
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -7,7 +7,7 @@ import { EnvironmentLabel, environmentTitle } from "~/components/environments/En
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { ButtonContent } from "~/components/primitives/Buttons";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
@@ -39,6 +39,7 @@ import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
import { Badge } from "~/components/primitives/Badge";
import { FirstEndpointSheet } from "./FirstEndpointSheet";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -202,7 +203,12 @@ export default function Page() {
</div>
))
) : (
<Paragraph>You have no clients yet</Paragraph>
<>
<Paragraph>Add your first endpoint</Paragraph>
<Paragraph>
<FirstEndpointSheet projectId={project.id} environments={environments} />
</Paragraph>
</>
)}
</div>
{selectedEndpoint && (
@@ -8,7 +8,7 @@ import { ProjectsMenu } from "~/components/navigation/ProjectsMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { projectPath } from "~/utils/pathBuilder";
@@ -33,7 +33,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
}
analytics.project.identify({ project });
telemetry.project.identify({ project });
return typedjson({
project,
@@ -6,7 +6,7 @@ import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { useOrganization } from "~/hooks/useOrganizations";
import { getOrganizationFromSlug } from "~/models/organization.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { commitCurrentOrgSession, setCurrentOrg } from "~/services/currentOrganization.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
@@ -25,7 +25,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
throw new Response("Not Found", { status: 404 });
}
analytics.organization.identify({ organization });
telemetry.organization.identify({ organization });
const session = await setCurrentOrg(organization.slug, request);
@@ -0,0 +1,71 @@
import { parse } from "@conform-to/zod";
import { ActionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import {
CreateEndpointError,
CreateEndpointService,
} from "~/services/endpoints/createEndpoint.server";
import { requireUserId } from "~/services/session.server";
import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core";
import { env } from "process";
import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server";
const ParamsSchema = z.object({
projectId: z.string(),
});
export const bodySchema = z.object({
environmentId: z.string(),
url: z.string().url("Must be a valid URL"),
});
export async function action({ request, params }: ActionArgs) {
const userId = await requireUserId(request);
const { projectId } = ParamsSchema.parse(params);
const formData = await request.formData();
const submission = parse(formData, { schema: bodySchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
const environment = await prisma.runtimeEnvironment.findUnique({
include: {
organization: true,
project: true,
},
where: {
id: submission.value.environmentId,
},
});
if (!environment) {
submission.error.environmentId = "Environment not found";
return json(submission);
}
const service = new ValidateCreateEndpointService();
const result = await service.call({
url: submission.value.url,
environment,
});
return json(submission);
} catch (e) {
if (e instanceof CreateEndpointError) {
submission.error.url = e.message;
return json(submission);
}
if (e instanceof Error) {
submission.error.url = `${e.name}: ${e.message}`;
} else {
submission.error.url = "Unknown error";
}
return json(submission, { status: 400 });
}
}
@@ -1,364 +0,0 @@
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import type { User } from "~/models/user.server";
class BehaviouralAnalytics {
client: PostHog | undefined = undefined;
constructor(apiKey?: string) {
if (!apiKey) {
console.log("No PostHog API key, so analytics won't track");
return;
}
this.client = new PostHog(apiKey, { host: "https://app.posthog.com" });
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.client === undefined) return;
this.client.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
//todo Job
// workflow = {
// identify: ({ workflow }: { workflow: Workflow }) => {
// if (this.client === undefined) return;
// this.client.groupIdentify({
// groupType: "workflow",
// groupKey: workflow.id,
// properties: {
// name: workflow.title,
// slug: workflow.slug,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// });
// },
// new: ({
// userId,
// organizationId,
// workflow,
// workflowCount,
// }: {
// userId: string;
// organizationId: string;
// workflow: Workflow;
// workflowCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow created",
// organizationId: organizationId,
// jobId: workflow.id,
// eventProperties: {
// id: workflow.id,
// slug: workflow.slug,
// title: workflow.title,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// userProperties: {
// workflowCount: workflowCount,
// },
// });
// },
// };
// workflowRun = {
// new: ({
// userId,
// organizationId,
// workflowId,
// workflowRun,
// environmentType,
// runCount,
// }: {
// userId: string;
// organizationId: string;
// workflowId: string;
// workflowRun: WorkflowRun;
// environmentType: string;
// runCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow run created",
// eventProperties: {
// id: workflowRun.id,
// workflowId: workflowRun.workflowId,
// environmentId: workflowRun.environmentId,
// environmentType,
// eventRuleId: workflowRun.eventRuleId,
// eventId: workflowRun.eventId,
// error: workflowRun.error,
// status: workflowRun.status,
// attemptCount: workflowRun.attemptCount,
// createdAt: workflowRun.createdAt,
// updatedAt: workflowRun.updatedAt,
// startedAt: workflowRun.startedAt,
// finishedAt: workflowRun.finishedAt,
// timedOutAt: workflowRun.timedOutAt,
// timedOutReason: workflowRun.timedOutReason,
// isTest: workflowRun.isTest,
// },
// userProperties: {
// runCount: runCount,
// },
// organizationId: organizationId,
// jobId: workflowId,
// environmentId: workflowRun.environmentId,
// });
// },
// };
environment = {
identify: ({ environment }: { environment: RuntimeEnvironment }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "environment",
groupKey: environment.id,
properties: {
name: environment.slug,
slug: environment.slug,
organizationId: environment.organizationId,
createdAt: environment.createdAt,
updatedAt: environment.updatedAt,
},
});
},
};
telemetry = {
capture: ({
userId,
event,
properties,
organizationId,
environmentId,
}: {
userId: string;
event: string;
properties: Record<string | number, any>;
organizationId?: string;
environmentId?: string;
}) => {
this.#capture({
userId,
event,
eventProperties: properties,
organizationId,
environmentId,
});
},
};
#capture(event: CaptureEvent) {
if (this.client === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.client.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const analytics = new BehaviouralAnalytics(env.POSTHOG_PROJECT_KEY);
+63 -4
View File
@@ -13,6 +13,8 @@ import {
RegisterTriggerBodySchema,
RunJobBody,
RunJobResponseSchema,
ValidateResponse,
ValidateResponseSchema,
} from "@trigger.dev/core";
import { safeBodyFromResponse, safeParseBodyFromResponse } from "~/utils/json";
import { logger } from "./logger.server";
@@ -25,16 +27,15 @@ export class EndpointApiError extends Error {
}
}
// TODO: this should work with tunnelling
export class EndpointApi {
constructor(private apiKey: string, private url: string, private id: string) {}
constructor(private apiKey: string, private url: string) {}
async ping(): Promise<PongResponse> {
async ping(endpointId: string): Promise<PongResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-endpoint-id": this.id,
"x-trigger-endpoint-id": endpointId,
"x-trigger-action": "PING",
},
});
@@ -271,6 +272,64 @@ export class EndpointApi {
return HttpSourceResponseSchema.parse(anyBody);
}
async validate(): Promise<ValidateResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-action": "VALIDATE",
},
});
if (!response) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}`,
};
}
if (response.status === 401) {
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
if (body) {
return {
ok: false,
error: body.message,
} as const;
}
return {
ok: false,
error: `Trigger API key is invalid`,
} as const;
}
if (!response.ok) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}. Status code: ${response.status}`,
};
}
const validateResponse = await safeParseBodyFromResponse(response, ValidateResponseSchema);
if (!validateResponse) {
return {
ok: false,
error: `Could not parse response from endpoint. Make sure it points to the correct URL (you might be missing /api/trigger)`,
};
}
if (!validateResponse.success) {
return {
ok: false,
error: `Endpoint ${this.url} responded with error: ${validateResponse.error.message}`,
};
}
return validateResponse.data;
}
}
async function safeFetch(url: string, options: RequestInit) {
@@ -34,9 +34,9 @@ export class CreateEndpointService {
}) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl, id);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const pong = await client.ping();
const pong = await client.ping(id);
if (!pong.ok) {
throw new CreateEndpointError("FAILED_PING", pong.error);
@@ -28,7 +28,7 @@ export class IndexEndpointService {
const endpoint = await findEndpoint(id);
// Make a request to the endpoint to fetch a list of jobs
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url, endpoint.slug);
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url);
const indexResponse = await client.indexEndpoint();
@@ -0,0 +1,44 @@
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
export class RecurringEndpointIndexService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(ts: Date) {
// Find all production endpoints that haven't been indexed in the last 10 minutes
const currentTimestamp = ts.getTime();
const endpoints = await this.#prismaClient.endpoint.findMany({
where: {
environment: {
type: RuntimeEnvironmentType.PRODUCTION,
},
indexings: {
none: {
createdAt: {
gt: new Date(currentTimestamp - 10 * 60 * 1000),
},
},
},
},
});
logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", {
count: endpoints.length,
});
// Enqueue each endpoint for indexing
for (const endpoint of endpoints) {
await workerQueue.enqueue("indexEndpoint", {
id: endpoint.id,
source: "INTERNAL",
});
}
}
}
@@ -0,0 +1,100 @@
import { customAlphabet } from "nanoid";
import { $transaction, prisma, PrismaClient } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { workerQueue } from "../worker.server";
import { CreateEndpointError } from "./createEndpoint.server";
import { EndpointApi } from "../endpointApi.server";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
export class ValidateCreateEndpointService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ environment, url }: { environment: AuthenticatedEnvironment; url: string }) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const validationResult = await client.validate();
if (!validationResult.ok) {
throw new Error(validationResult.error);
}
try {
const result = await $transaction(this.#prismaClient, async (tx) => {
const endpoint = await tx.endpoint.upsert({
where: {
environmentId_slug: {
environmentId: environment.id,
slug: validationResult.endpointId,
},
},
create: {
environment: {
connect: {
id: environment.id,
},
},
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
slug: validationResult.endpointId,
url: endpointUrl,
indexingHookIdentifier: indexingHookIdentifier(),
},
update: {
url: endpointUrl,
},
});
// Kick off process to fetch the jobs for this endpoint
await workerQueue.enqueue(
"indexEndpoint",
{
id: endpoint.id,
source: "INTERNAL",
},
{ tx }
);
return endpoint;
});
return result;
} catch (error) {
if (error instanceof Error) {
throw new CreateEndpointError("FAILED_UPSERT", error.message);
} else {
throw new CreateEndpointError("FAILED_UPSERT", "Something went wrong");
}
}
}
// If the endpoint URL points to localhost, and the RUNTIME_PLATFORM is docker-compose, then we need to rewrite the host to host.docker.internal
// otherwise we shouldn't change anything
#normalizeEndpointUrl(url: string) {
if (env.RUNTIME_PLATFORM === "docker-compose") {
const urlObj = new URL(url);
if (urlObj.hostname === "localhost") {
urlObj.hostname = "host.docker.internal";
return urlObj.toString();
}
}
return url;
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import type { User } from "~/models/user.server";
import { analytics } from "./analytics.server";
import { telemetry } from "./telemetry.server";
export async function postAuthentication({
user,
@@ -10,5 +10,5 @@ export async function postAuthentication({
loginMethod: User["authenticationMethod"];
isNewUser: boolean;
}) {
analytics.user.identify({ user, isNewUser });
telemetry.user.identify({ user, isNewUser });
}
@@ -54,7 +54,7 @@ export class PerformRunExecutionService {
async #executePreprocessing(execution: FoundRunExecution) {
const { run } = execution;
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url, run.endpoint.slug);
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const startedAt = new Date();
@@ -189,7 +189,7 @@ export class PerformRunExecutionService {
return;
}
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url, run.endpoint.slug);
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const startedAt = new Date();
@@ -55,8 +55,7 @@ export class DeliverHttpSourceRequestService {
const clientApi = new EndpointApi(
httpSourceRequest.environment.apiKey,
httpSourceRequest.endpoint.url,
httpSourceRequest.endpoint.slug
httpSourceRequest.endpoint.url
);
const { response, events } = await clientApi.deliverHttpSourceRequest({
@@ -0,0 +1,239 @@
import { TriggerClient } from "@trigger.dev/sdk";
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { User } from "~/models/user.server";
type Options = {
postHogApiKey?: string;
trigger?: {
apiKey: string;
apiUrl: string;
};
};
class Telemetry {
#posthogClient: PostHog | undefined = undefined;
#triggerClient: TriggerClient | undefined = undefined;
constructor({ postHogApiKey, trigger }: Options) {
if (postHogApiKey) {
this.#posthogClient = new PostHog(postHogApiKey, { host: "https://app.posthog.com" });
} else {
console.log("No PostHog API key, so analytics won't track");
}
if (trigger) {
this.#triggerClient = new TriggerClient({
id: "triggerdotdev",
apiKey: trigger.apiKey,
apiUrl: trigger.apiUrl,
});
console.log("Created telemetry TriggerClient");
}
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
this.#triggerClient?.sendEvent({
name: "user.created",
payload: {
userId: user.id,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.#posthogClient === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.#posthogClient === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
#capture(event: CaptureEvent) {
if (this.#posthogClient === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.#posthogClient.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const telemetry = new Telemetry({
postHogApiKey: env.POSTHOG_PROJECT_KEY,
trigger:
env.TELEMETRY_TRIGGER_API_KEY && env.TELEMETRY_TRIGGER_API_URL
? {
apiKey: env.TELEMETRY_TRIGGER_API_KEY,
apiUrl: env.TELEMETRY_TRIGGER_API_URL,
}
: undefined,
});
@@ -45,7 +45,7 @@ export class InitializeTriggerService {
},
});
const clientApi = new EndpointApi(environment.apiKey, endpoint.url, endpoint.slug);
const clientApi = new EndpointApi(environment.apiKey, endpoint.url);
const registerMetadata = await clientApi.initializeTrigger(dynamicTrigger.slug, payload.params);
+27 -1
View File
@@ -6,6 +6,7 @@ import { env } from "~/env.server";
import { ZodWorker } from "~/platform/zodWorker.server";
import { sendEmail } from "./email.server";
import { IndexEndpointService } from "./endpoints/indexEndpoint.server";
import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server";
import { DeliverEventService } from "./events/deliverEvent.server";
import { InvokeDispatcherService } from "./events/invokeDispatcher.server";
import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server";
@@ -95,6 +96,31 @@ function getWorkerQueue() {
pollInterval: 1000,
},
schema: workerCatalog,
recurringTasks: {
// Run this every 5 minutes
autoIndexProductionEndpoints: {
pattern: "*/5 * * * *",
handler: async (payload, job) => {
const service = new RecurringEndpointIndexService();
await service.call(payload.ts);
},
},
// Run this every hour
purgeOldIndexings: {
pattern: "0 * * * *",
handler: async (payload, job) => {
// Delete indexings that are older than 7 days
await prisma.endpointIndex.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
});
},
},
},
tasks: {
"events.invokeDispatcher": {
maxAttempts: 3,
@@ -154,7 +180,7 @@ function getWorkerQueue() {
},
},
deliverHttpSourceRequest: {
maxAttempts: 5,
maxAttempts: 25,
handler: async (payload, job) => {
const service = new DeliverHttpSourceRequestService();
+1
View File
@@ -61,6 +61,7 @@
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/sdk": "workspace:2.0.5",
"@uiw/react-codemirror": "^4.19.5",
"class-variance-authority": "^0.5.2",
"clsx": "^1.2.1",
+7 -4
View File
@@ -4,21 +4,24 @@ description: "An ever-growing list of example Jobs which you can use to get star
---
<Info>
If you are using integrations, you'll need set up authentication either using
OAuth or API keys / access tokens. You can find out how to do that in the
[integrations section](/integrations).
If you are using integrations, you'll need set up authentication either using OAuth or API keys /
access tokens. You can find out how to do that in the [integrations section](/integrations).
</Info>
Click the links below to view the Job code. You can also easily test these Jobs by following the instructions in the README of each example.
| Job (code in link) | Description | Integrations used |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [Basic delay](https://github.com/triggerdotdev/examples/blob/main/delays/src/jobs/delayJob.ts) | Logs a message to the console, waits for 5 minutes, and then logs another message. | N/A |
| [Basic interval](https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/interval.ts) | This Job will run every 60 seconds, starting 60 seconds after this Job is first indexed. | N/A |
| [Cron scheduled interval](https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/cronScheduled.ts) | A scheduled Job which runs at 2:30pm every Monday. | N/A |
| [OpenAI text summarizer](https://github.com/triggerdotdev/examples/blob/main/openai-text-summarizer/src/jobs/textSummarizer.ts) | Summarizes a block of text, pulling out the most unique and helpful points using OpenAI GPT-3.5 turbo. | [OpenAI](/integrations/apis/openai) |
| [Tell me a joke using OpenAI](https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/tellMeAJoke.ts) | Generates a random joke using OpenAI GPT 3.5. | [OpenAI](/integrations/apis/openai) |
| [Generate an image using OpenAI](https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/generateHedgehogImages.ts) | Generates a random image of a hedgehog using OpenAI DALL-E. | [OpenAI](/integrations/apis/openai) |
| [GitHub issue reminder](https://github.com/triggerdotdev/examples/blob/main/github-issue-reminder/jobs/githubIssue.ts) | Sends a Slack message to a channel if a GitHub issue is left open for 24 hours | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Github new star alert in Slack](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts) | When a repo is starred, a message is sent to a Slack channel with the name and URL of the GitHub user who starred the repo, and the updated Stargazers count. | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Add a custom label to a GitHub issue when it is created](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/onIssueOpened.ts) | When a new GitHub issue is opened it adds a "Bug" label to it. | [GitHub](/integrations/apis/github) |
| [GitHub new star alert](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarAlert.ts) | When a repo is starred a message is logged with the new Stargazers count. | [GitHub](/integrations/apis/github) |
| [Github new star alert in Slack](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts) | When a repo is starred, a message is sent to a Slack channel with the name and URL of the GitHub user who starred the repo, and the updated Stargazers count. | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Send a Slack message when an event is received](https://github.com/triggerdotdev/examples/blob/main/slack/src/jobs/sendSlackMessage.ts) | Sends a Slack message to a specific channel when an event is received. | [Slack](/integrations/apis/slack) |
| [Send an email using Resend](https://github.com/triggerdotdev/examples/blob/main/resend/src/jobs/resendBasicEmail.ts) | Send a basic email using Resend | [Resend](/integrations/apis/resend) |
+15 -23
View File
@@ -7,8 +7,8 @@ description: "Interact with your Supabase project using the Supabase JS Client."
Our `@trigger.dev/supabase` package provides an integration that wraps the [@supabase/supabase-js](https://github.com/supabase/supabase-js) package, allowing you to run tasks to interact with your Supabase project.
<Note>
If you want to trigger jobs based on changes in your Supabase database, you'll
need to use the [Supabase Management API](../management) integration
If you want to trigger jobs based on changes in your Supabase database, you'll need to use the
[Supabase Management API](/integrations/apis/supabase/management) integration
</Note>
## Usage
@@ -26,8 +26,7 @@ const supabase = new Supabase({
```
<Warning>
Never expose the `service_role` key in a browser or anywhere where a user can
see it.
Never expose the `service_role` key in a browser or anywhere where a user can see it.
</Warning>
You can then use the `supabase` integration to run tasks in your jobs:
@@ -39,21 +38,17 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const { data: users, error } = await io.supabase.runTask(
"find-users",
async (db) => {
return db.from("users").select("*");
}
);
const { data: todos, error } = await io.supabase.runTask("find-todos", async (db) => {
return db.from("todos").select("*");
});
},
});
```
<Note>
By using `runTask` instead of the `@supabase/supabase-js` client directly
inside your job run, you'll be able to create tasks that can be run
idempotently and also retried. For more, see our guide on
[Resumability](http://localhost:3050/documentation/concepts/resumability)
By using `runTask` instead of the `@supabase/supabase-js` client directly inside your job run,
you'll be able to create tasks that can be run idempotently and also retried. For more, see our
guide on [Resumability](http://localhost:3050/documentation/concepts/resumability)
</Note>
You can also choose to throw an error if the query fails and abort the job run:
@@ -65,8 +60,8 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const users = await io.supabase.runTask("find-users", async (db) => {
const { data, error } = await db.from("users").select("*");
const todos = await io.supabase.runTask("find-todos", async (db) => {
const { data, error } = await db.from("todos").select("*");
if (error) throw error;
@@ -83,10 +78,7 @@ The `db` object passed to the callback is an instance of the [@supabase/supabase
- [Invoking Functions](https://supabase.com/docs/reference/javascript/functions-invoke)
- [Storage](https://supabase.com/docs/reference/javascript/storage-createbucket)
<Warning>
Currently we do not support Supabase Realtime (such as subscribing to a
channel)
</Warning>
<Warning>Currently we do not support Supabase Realtime (such as subscribing to a channel)</Warning>
## Typescript Support
@@ -108,15 +100,15 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const users = await io.supabase.runTask("find-users", async (db) => {
const { data, error } = await db.from("users").select("*");
const todos = await io.supabase.runTask("find-todos", async (db) => {
const { data, error } = await db.from("todos").select("*");
if (error) throw error;
return data;
});
// users is now typed as User[] instead of any[]
// todos is now typed as Todo[] instead of any[]
},
});
```
+28 -17
View File
@@ -126,7 +126,7 @@ client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onInserted({
table: "users",
table: "todos",
}),
run: async (payload, io, ctx) => {
// payload is the database webhook body (see https://supabase.com/docs/guides/database/webhooks#payload)
@@ -137,20 +137,6 @@ client.defineJob({
You can add additional filters to the trigger by passing a `filter` object:
```ts
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onUpdated({
table: "users",
filter: {
country: ["USA", "Canada"], // This will only trigger the job if the user.country is USA or Canada
},
}),
run: async (payload, io, ctx) => {
// payload is the database webhook body (see https://supabase.com/docs/guides/database/webhooks#payload)
},
});
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
@@ -172,6 +158,31 @@ client.defineJob({
});
```
You can also listen for multiple different events using the `on` trigger:
```ts
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.on({
table: "todos",
events: ["INSERT", "UPDATE"] // Trigger on both insert and update events
filter: {
record: {
is_completed: [false],
},
},
}),
run: async (payload, io, ctx) => {
if (payload.type === "INSERT") {
// payload will be typed as the INSERT payload
} else {
// payload will be typed as the UPDATE payload
}
},
});
```
<Note>
We will only create at most 1 database webhook per table, to limit resource usage when writing to
your database. This means we cannot support scoping updated triggers to specific columns.
@@ -196,10 +207,10 @@ client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onUpdated({
table: "users",
table: "todos",
}),
run: async (payload, io, ctx) => {
// payload.record and payload.old_record are now correctly typed to match the users table
// payload.record and payload.old_record are now correctly typed to match the todos table
},
});
```
+10 -27
View File
@@ -23,7 +23,8 @@
},
"feedback": {
"suggestEdit": true,
"raiseIssue": true
"raiseIssue": true,
"thumbsRating": true
},
"topbarCtaButton": {
"type": "github",
@@ -154,10 +155,7 @@
},
{
"group": "Overview",
"pages": [
"integrations/introduction",
"integrations/create"
]
"pages": ["integrations/introduction", "integrations/create"]
},
{
"group": "Integrations",
@@ -180,22 +178,16 @@
},
{
"group": "OpenAI",
"pages": [
"integrations/apis/openai"
]
"pages": ["integrations/apis/openai"]
},
"integrations/apis/plain",
{
"group": "Resend",
"pages": [
"integrations/apis/resend"
]
"pages": ["integrations/apis/resend"]
},
{
"group": "Slack",
"pages": [
"integrations/apis/slack"
]
"pages": ["integrations/apis/slack"]
},
"integrations/apis/typeform"
]
@@ -249,10 +241,7 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
}
]
},
@@ -263,10 +252,7 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
}
]
},
@@ -287,10 +273,7 @@
},
{
"group": "Overview",
"pages": [
"examples/introduction",
"examples/examples-repository"
]
"pages": ["examples/introduction", "examples/examples-repository"]
}
],
"footerSocials": {
@@ -303,4 +286,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+11 -1
View File
@@ -2,8 +2,18 @@
This project is meant to be used to create a catalog of jobs, usually to test something in an integration or the SDK.
## Setup
You will need to create a `.env` file. You can duplicate the `.env.example` file and set your local `TRIGGER_API_KEY` value.
### Running
You need to build the CLI:
```sh
pnpm run build --filter @trigger.dev/cli
```
Each file in `src` is a separate set of jobs that can be run separately. For example, the `src/stripe.ts` file can be run with:
```sh
@@ -15,7 +25,7 @@ This will open up a local server using `express` on port 8080. Then in a new ter
```sh
cd examples/job-catalog
pnpm run trigger:dev
pnpm run dev:trigger
```
### Adding a new file
+91 -8
View File
@@ -1,14 +1,19 @@
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { TriggerClient } from "@trigger.dev/sdk";
import { createExpressServer } from "@trigger.dev/express";
import { z } from "zod";
import { SupabaseManagement } from "@trigger.dev/supabase";
import { Supabase, SupabaseManagement } from "@trigger.dev/supabase";
const supabaseManagement = new SupabaseManagement({
id: "supabase-management",
apiKey: process.env["SUPABASE_API_KEY"]!,
});
const db = supabaseManagement.db(process.env["SUPABASE_ID"]!);
const triggers = supabaseManagement.db<Database>(process.env["SUPABASE_ID"]!);
const supabase = new Supabase({
id: "supabase",
supabaseKey: process.env["SUPABASE_SERVICE_ROLE_KEY"]!,
supabaseUrl: process.env["SUPABASE_URL"]!,
});
export const client = new TriggerClient({
id: "job-catalog",
@@ -24,8 +29,8 @@ client.defineJob({
id: "supabase-management-example-1",
name: "Supabase Management Example 1",
version: "0.1.0",
trigger: db.onInserted({
table: "users",
trigger: triggers.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {},
});
@@ -34,8 +39,86 @@ client.defineJob({
id: "supabase-management-example-2",
name: "Supabase Management Example 2",
version: "0.1.0",
trigger: db.onUpdated({
table: "users",
trigger: triggers.onUpdated({
table: "todos",
}),
run: async (payload, io, ctx) => {},
});
client.defineJob({
id: "supabase-management-example-on",
name: "Supabase Management Example On",
version: "0.1.0",
trigger: triggers.on({
table: "todos",
events: ["INSERT", "UPDATE"],
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {
const user = await io.supabase.runTask("fetch-user", async (db) => {
const { data, error } = await db.auth.admin.getUserById(payload.record.user_id);
if (error) {
throw error;
}
return data.user;
});
return user;
},
});
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
export interface Database {
public: {
Tables: {
todos: {
Row: {
id: number;
inserted_at: string;
is_complete: boolean | null;
task: string | null;
user_id: string;
};
Insert: {
id?: number;
inserted_at?: string;
is_complete?: boolean | null;
task?: string | null;
user_id: string;
};
Update: {
id?: number;
inserted_at?: string;
is_complete?: boolean | null;
task?: string | null;
user_id?: string;
};
Relationships: [
{
foreignKeyName: "todos_user_id_fkey";
columns: ["user_id"];
referencedRelation: "users";
referencedColumns: ["id"];
},
];
};
};
Views: {
[_ in never]: never;
};
Functions: {
[_ in never]: never;
};
Enums: {
[_ in never]: never;
};
CompositeTypes: {
[_ in never]: never;
};
};
}
+15
View File
@@ -1,5 +1,20 @@
# @trigger.dev/github
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "2.0.3",
"version": "2.0.5",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -29,8 +29,8 @@
"@octokit/request": "^6.2.5",
"@octokit/request-error": "^4.0.1",
"@octokit/webhooks": "^10.4.0",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.5",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"octokit": "^2.0.14",
"zod": "3.21.4"
},
+15
View File
@@ -1,5 +1,20 @@
# @trigger.dev/slack
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "2.0.3",
"version": "2.0.5",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
},
"dependencies": {
"openai": "^3.3.0",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.5",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"zod": "3.21.4"
},
"engines": {
+15
View File
@@ -1,5 +1,20 @@
# @trigger.dev/plain
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "2.0.3",
"version": "2.0.5",
"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.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"@trigger.dev/sdk": "workspace:^2.0.5",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+15
View File
@@ -1,5 +1,20 @@
# @trigger.dev/resend
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "2.0.3",
"version": "2.0.5",
"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.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"@trigger.dev/sdk": "workspace:^2.0.5",
"resend": "^0.9.1"
},
"engines": {
+13
View File
@@ -1,5 +1,18 @@
# @trigger.dev/slack
## 2.0.5
### Patch Changes
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
## 2.0.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "2.0.3",
"version": "2.0.5",
"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.0.3",
"@trigger.dev/sdk": "workspace:^2.0.5",
"zod": "3.21.4"
},
"engines": {
+15
View File
@@ -1,5 +1,20 @@
# @trigger.dev/stripe
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "2.0.3",
"version": "2.0.5",
"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.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"@trigger.dev/sdk": "workspace:^2.0.5",
"stripe": "^12.14.0",
"zod": "3.21.4"
},
+16
View File
@@ -1,5 +1,21 @@
# @trigger.dev/supabase
## 2.0.5
### Patch Changes
- 3cf3eaff: You can now trigger on multiple database events in the same job
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "2.0.3",
"version": "2.0.5",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"@trigger.dev/sdk": "workspace:^2.0.5",
"supabase-management-js": "^0.1.4",
"zod": "3.21.4"
},
+183 -2
View File
@@ -34,6 +34,91 @@ class SupabaseDatabase<Database = any> {
private projectRef: string
) {}
/**
* The function `on` creates a trigger for when a record is inserted, updated, or deleted on a
* specific table in a database schema.
* @param params - The `params` parameter is an object that contains the following properties:
* @param params.table - The `table` property is a string that specifies the name of the table
* that the trigger will be created for.
* @param params.events - The `events` property is an array of events that specifies the events
* that the trigger will be called for. The events that can be specified are `INSERT`, `UPDATE`, or `DELETE`.
* By default, the trigger will be called for all events.
* @param params.schema - The `schema` property is a string that specifies the name of the schema
* that the trigger will be created for. If the schema is not specified, the default schema will
* be used. (public)
* @param params.filter - The `filter` property is an object that specifies the filter that will
* be used to determine if the trigger should be called. If the filter is not specified, the
* trigger will be called for all records.
*
* @example
*
* ```ts
* const supabase = new SupabaseManagement({ id: "supabase" });
* const database = supabase.database<Database>("https://<project-id>.supabase.co");
*
* client.defineJob({
* trigger: database.on({
* table: "todos",
* events: ["INSERTED", "UPDATED"],
* schema: "public",
* filter: {
* record: { is_completed: [false] },
* },
* }),
* })
* ```
*/
on<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any,
TTableName extends string & keyof Schema["Tables"] = string & keyof Schema["Tables"],
TTable extends Schema["Tables"][TTableName] = Schema["Tables"][TTableName],
TEvents extends WebhookEvents[] = ["INSERT", "UPDATE", "DELETE"],
>(params: { table: TTableName; events?: TEvents; schema?: SchemaName; filter?: EventFilter }) {
return createTrigger<Prettify<UnionPayloads<TEvents, TTableName, SchemaName, TTable["Row"]>>>(
this.integration.source,
{
event: params.events ?? ["INSERT", "UPDATE", "DELETE"],
projectRef: this.projectRef,
...params,
}
);
}
/**
* The function `onInserted` creates a trigger for when a new record is inserted into a specific
* table in a database schema.
* @param params - The `params` parameter is an object that contains the following properties:
* @param params.table - The `table` property is a string that specifies the name of the table
* that the trigger will be created for.
* @param params.schema - The `schema` property is a string that specifies the name of the schema
* that the trigger will be created for. If the schema is not specified, the default schema will
* be used. (public)
* @param params.filter - The `filter` property is an object that specifies the filter that will
* be used to determine if the trigger should be called. If the filter is not specified, the
* trigger will be called for all records.
*
* @example
*
* ```ts
* const supabase = new SupabaseManagement({ id: "supabase" });
* const database = supabase.database<Database>("https://<project-id>.supabase.co");
*
* client.defineJob({
* trigger: database.onInserted({
* table: "todos",
* schema: "public",
* filter: {
* record: { is_completed: [false] },
* },
* }),
* })
* ```
*/
onInserted<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
@@ -57,6 +142,37 @@ class SupabaseDatabase<Database = any> {
});
}
/**
* The function `onUpdated` creates a trigger for when a new record is updated on a specific
* table in a database schema.
* @param params - The `params` parameter is an object that contains the following properties:
* @param params.table - The `table` property is a string that specifies the name of the table
* that the trigger will be created for.
* @param params.schema - The `schema` property is a string that specifies the name of the schema
* that the trigger will be created for. If the schema is not specified, the default schema will
* be used. (public)
* @param params.filter - The `filter` property is an object that specifies the filter that will
* be used to determine if the trigger should be called. If the filter is not specified, the
* trigger will be called for all records.
*
* @example
*
* ```ts
* const supabase = new SupabaseManagement({ id: "supabase" });
* const database = supabase.database<Database>("https://<project-id>.supabase.co");
*
* client.defineJob({
* trigger: database.onUpdated({
* table: "todos",
* schema: "public",
* filter: {
* record: { completed: [true] },
* old_record: { completed: [false] },
* },
* }),
* })
* ```
*/
onUpdated<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
@@ -80,6 +196,36 @@ class SupabaseDatabase<Database = any> {
});
}
/**
* The function `onDeleted` creates a trigger for when a new record is deleted from a specific
* table in a database schema.
* @param params - The `params` parameter is an object that contains the following properties:
* @param params.table - The `table` property is a string that specifies the name of the table
* that the trigger will be created for.
* @param params.schema - The `schema` property is a string that specifies the name of the schema
* that the trigger will be created for. If the schema is not specified, the default schema will
* be used. (public)
* @param params.filter - The `filter` property is an object that specifies the filter that will
* be used to determine if the trigger should be called. If the filter is not specified, the
* trigger will be called for all records.
*
* @example
*
* ```ts
* const supabase = new SupabaseManagement({ id: "supabase" });
* const database = supabase.database<Database>("https://<project-id>.supabase.co");
*
* client.defineJob({
* trigger: database.onDeleted({
* table: "todos",
* schema: "public",
* filter: {
* old_record: { is_completed: [true] },
* },
* }),
* })
* ```
*/
onDeleted<
SchemaName extends string & keyof Database = "public" extends keyof Database
? "public"
@@ -175,9 +321,44 @@ type WebhookEventSource = ReturnType<typeof createWebhookEventSource>;
type WebhookEvents = "INSERT" | "UPDATE" | "DELETE";
type WebhookEventPayloads<
TTableName extends string,
TSchemaName extends string = "public",
TRecord = any,
> = {
INSERT: {
table: TTableName;
record: Prettify<TRecord>;
type: "INSERT";
schema: TSchemaName;
old_record: null;
};
UPDATE: {
table: TTableName;
record: Prettify<TRecord>;
type: "UPDATE";
schema: TSchemaName;
old_record: Prettify<TRecord>;
};
DELETE: {
table: TTableName;
record: null;
type: "DELETE";
schema: TSchemaName;
old_record: Prettify<TRecord>;
};
};
type UnionPayloads<
T extends WebhookEvents[],
TTableName extends string,
TSchemaName extends string = "public",
TRecord = any,
> = WebhookEventPayloads<TTableName, TSchemaName, TRecord>[T[number]];
function createTrigger<TEvent extends any>(
source: WebhookEventSource,
params: { event: WebhookEvents; filter?: EventFilter } & {
params: { event: WebhookEvents | WebhookEvents[]; filter?: EventFilter } & {
projectRef: string;
table: string;
schema?: string;
@@ -190,7 +371,7 @@ function createTrigger<TEvent extends any>(
icon: "supabase",
filter: {
...params.filter,
type: [params.event],
type: typeof params.event === "string" ? [params.event] : params.event,
schema: [params.schema ?? "public"],
},
properties: [],
+15
View File
@@ -1,5 +1,20 @@
# @trigger.dev/typeform
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "2.0.3",
"version": "2.0.5",
"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.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.5",
"@trigger.dev/integration-kit": "workspace:^2.0.5",
"zod": "3.21.4"
},
"engines": {
+10
View File
@@ -1,5 +1,15 @@
# create-trigger
## 2.0.5
## 2.0.4
### Patch Changes
- ff04bf44: Detect package manager from artifacts if they exist
- d5b8f829: The cli init command creates a jobs/index file and that is used to import jobs
- e7402978: Detect Next.js project by looking at dependencies if can't find next.config.js
## 2.0.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/cli",
"version": "2.0.3",
"version": "2.0.5",
"description": "The Trigger.dev CLI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+39 -12
View File
@@ -30,10 +30,12 @@ export type InitCommandOptions = {
type ResolvedOptions = Required<InitCommandOptions>;
export const initCommand = async (options: InitCommandOptions) => {
renderTitle();
telemetryClient.init.started(options);
const resolvedPath = resolvePath(options.projectPath);
await renderTitle(resolvedPath);
if (options.triggerUrl === CLOUD_TRIGGER_URL) {
logger.info(`✨ Initializing project in Trigger.dev Cloud`);
} else if (typeof options.triggerUrl === "string") {
@@ -42,7 +44,6 @@ export const initCommand = async (options: InitCommandOptions) => {
logger.info(`✨ Initializing Trigger.dev in project`);
}
const resolvedPath = resolvePath(options.projectPath);
// Detect if are are in a Next.js project
const isNextJsProject = await detectNextJsProject(resolvedPath);
@@ -436,10 +437,11 @@ async function createTriggerAppRoute(
const tsConfigPath = pathModule.join(projectPath, configFileName);
const { tsconfig } = await parse(tsConfigPath);
const extension = isTypescriptProject ? ".ts" : ".js";
const triggerFileName = `trigger${extension}`;
const examplesFileName = `examples${extension}`;
const routeFileName = `route${extension}`;
const extension = isTypescriptProject ? ".ts" : ".js"
const triggerFileName = `trigger${extension}`
const examplesFileName = `examples${extension}`
const examplesIndexFileName = `index${extension}`
const routeFileName = `route${extension}`
const pathAlias = getPathAlias(tsconfig, usesSrcDir);
const routePathPrefix = pathAlias ? pathAlias + "/" : "../../../";
@@ -448,8 +450,8 @@ async function createTriggerAppRoute(
import { createAppRoute } from "@trigger.dev/nextjs";
import { client } from "${routePathPrefix}trigger";
// Replace this with your own jobs
import "${routePathPrefix}jobs/examples";
import "${routePathPrefix}jobs";
//this route is used to send and receive data with Trigger.dev
export const { POST, dynamic } = createAppRoute(client);
@@ -489,6 +491,12 @@ client.defineJob({
});
`;
const examplesIndexContent = `
// import all your job files here
export * from "./examples"
`
const directories = pathModule.join(path, "app", "api", "trigger");
await fs.mkdir(directories, { recursive: true });
@@ -519,6 +527,11 @@ client.defineJob({
if (!exampleFileExists) {
await fs.writeFile(pathModule.join(exampleDirectories, examplesFileName), jobsContent);
await fs.writeFile(
pathModule.join(exampleDirectories, examplesIndexFileName),
examplesIndexContent
);
logger.success(
`✅ Created example job at ${usesSrcDir ? "src/" : ""}jobs/examples/examplesFileName`
);
@@ -539,14 +552,17 @@ async function createTriggerPageRoute(
const pathAlias = getPathAlias(tsconfig, usesSrcDir);
const routePathPrefix = pathAlias ? pathAlias + "/" : "../..";
const extension = isTypescriptProject ? ".ts" : ".js";
const triggerFileName = `trigger${extension}`;
const examplesFileName = `examples${extension}`;
const extension = isTypescriptProject ? ".ts" : ".js"
const triggerFileName = `trigger${extension}`
const examplesFileName = `examples${extension}`
const examplesIndexFileName = `index${extension}`
const routeContent = `
import { createPagesRoute } from "@trigger.dev/nextjs";
import { client } from "${routePathPrefix}trigger";
import "${routePathPrefix}jobs";
//this route is used to send and receive data with Trigger.dev
const { handler, config } = createPagesRoute(client);
export { config };
@@ -588,6 +604,12 @@ client.defineJob({
});
`;
const examplesIndexContent = `
// import all your job files here
export * from "./examples"
`
const directories = pathModule.join(path, "pages", "api");
await fs.mkdir(directories, { recursive: true });
@@ -620,6 +642,11 @@ client.defineJob({
if (!exampleFileExists) {
await fs.writeFile(pathModule.join(exampleDirectories, examplesFileName), jobsContent);
await fs.writeFile(
pathModule.join(exampleDirectories, examplesIndexFileName),
examplesIndexContent
);
logger.success(
`✅ Created example job at ${usesSrcDir ? "src/" : ""}jobs/examples/${examplesFileName}`
);
+2 -2
View File
@@ -2,7 +2,7 @@ import chalk from "chalk";
import { execa } from "execa";
import ora, { type Ora } from "ora";
import pathModule from "path";
import { getUserPkgManager, type PackageManager } from "./getUserPkgManager.js";
import { getUserPackageManager, type PackageManager } from "./getUserPkgManager.js";
import fs from "fs/promises";
import fetch from "node-fetch";
import { z } from "zod";
@@ -18,7 +18,7 @@ export type InstalledPackage = {
};
export async function addDependencies(projectDir: string, packages: Array<InstallPackage>) {
const pkgManager = getUserPkgManager();
const pkgManager = await getUserPackageManager(projectDir);
const spinner = ora("Adding @trigger.dev dependencies to package.json...").start();
+20 -6
View File
@@ -1,15 +1,29 @@
import fs from "fs/promises";
import pathModule from "path";
import { readPackageJson } from "./readPackageJson.js";
/** Detects if the project is a Next.js project at path */
export async function detectNextJsProject(path: string): Promise<boolean> {
// Checks for the presence of a next.config.js file
try {
// Check if next.config.js file exists in the given path
await fs.access(pathModule.join(path, "next.config.js"));
const hasNextConfigFile = await detectNextConfigFile(path);
if (hasNextConfigFile) {
return true;
} catch (error) {
// If next.config.js file doesn't exist, it's not a Next.js project
}
return await detectNextDependency(path);
}
async function detectNextConfigFile(path: string): Promise<boolean> {
return fs
.access(pathModule.join(path, "next.config.js"))
.then(() => true)
.catch(() => false);
}
async function detectNextDependency(path: string): Promise<boolean> {
const packageJsonContent = await readPackageJson(path);
if (!packageJsonContent) {
return false;
}
return packageJsonContent.dependencies?.next !== undefined;
}
+31 -2
View File
@@ -1,6 +1,17 @@
import pathModule from "path";
import { pathExists } from "./fileSystem.js";
export type PackageManager = "npm" | "pnpm" | "yarn";
export const getUserPkgManager: () => PackageManager = () => {
export async function getUserPackageManager(path: string): Promise<PackageManager> {
try {
return detectPackageManagerFromArtifacts(path);
} catch (error) {
return detectPackageManagerFromCurrentCommand();
}
}
function detectPackageManagerFromCurrentCommand(): PackageManager {
// This environment variable is set by npm and yarn but pnpm seems less consistent
const userAgent = process.env.npm_config_user_agent;
@@ -16,4 +27,22 @@ export const getUserPkgManager: () => PackageManager = () => {
// If no user agent is set, assume npm
return "npm";
}
};
}
async function detectPackageManagerFromArtifacts(path: string): Promise<PackageManager> {
const packageFiles = [
{ name: "yarn.lock", pm: "yarn" } as const,
{ name: "pnpm-lock.yaml", pm: "pnpm" } as const,
{ name: "package-lock.json", pm: "npm" } as const,
{ name: "npm-shrinkwrap.json", pm: "npm" } as const,
];
for (const { name, pm } of packageFiles) {
const exists = await pathExists(pathModule.join(path, name));
if (exists) {
return pm;
}
}
throw new Error("Could not detect package manager from artifacts");
}
@@ -1,4 +1,4 @@
import { getUserPkgManager, type PackageManager } from "./getUserPkgManager.js";
import { getUserPackageManager, type PackageManager } from "./getUserPkgManager.js";
import { logger } from "./logger.js";
import ora, { type Ora } from "ora";
import chalk from "chalk";
@@ -7,7 +7,7 @@ import { execa } from "execa";
export async function installDependencies(projectDir: string) {
logger.info("Installing dependencies...");
const pkgManager = getUserPkgManager();
const pkgManager = await getUserPackageManager(projectDir);
const installSpinner = await runInstallCommand(pkgManager, projectDir);
+10
View File
@@ -0,0 +1,10 @@
import pathModule from "path";
import { type PackageJson } from "type-fest";
import { readJSONFile } from "./fileSystem.js";
export async function readPackageJson(directory: string): Promise<PackageJson | undefined> {
const packageJsonPath = pathModule.join(directory, "package.json");
return readJSONFile(packageJsonPath)
.then((f) => f as PackageJson)
.catch(() => undefined);
}
+3 -3
View File
@@ -1,6 +1,6 @@
import gradient from "gradient-string";
import { TITLE_TEXT } from "../consts.js";
import { getUserPkgManager } from "./getUserPkgManager.js";
import { getUserPackageManager } from "./getUserPkgManager.js";
// colors brought in from vscode poimandres theme
const poimandresTheme = {
@@ -12,11 +12,11 @@ const poimandresTheme = {
yellow: "#fffac2",
};
export const renderTitle = () => {
export const renderTitle = async (projectDirectory: string) => {
const triggerGradient = gradient(Object.values(poimandresTheme));
// resolves weird behavior where the ascii is offset
const pkgManager = getUserPkgManager();
const pkgManager = await getUserPackageManager(projectDirectory);
if (pkgManager === "yarn" || pkgManager === "pnpm") {
console.log("");
}
+8
View File
@@ -1,5 +1,13 @@
# internal-platform
## 2.0.5
## 2.0.4
### Patch Changes
- 96384991: Adding the validate endpoint action to be able to add an endpoint first in the dashboard
## 2.0.3
## 2.0.2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/core",
"version": "2.0.3",
"version": "2.0.5",
"description": "Common code used across the Trigger.dev SDK and platform",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+17
View File
@@ -128,6 +128,23 @@ export const PongResponseSchema = z.discriminatedUnion("ok", [
export type PongResponse = z.infer<typeof PongResponseSchema>;
export const ValidateSuccessResponseSchema = z.object({
ok: z.literal(true),
endpointId: z.string(),
});
export const ValidateErrorResponseSchema = z.object({
ok: z.literal(false),
error: z.string(),
});
export const ValidateResponseSchema = z.discriminatedUnion("ok", [
ValidateSuccessResponseSchema,
ValidateErrorResponseSchema,
]);
export type ValidateResponse = z.infer<typeof ValidateResponseSchema>;
export const QueueOptionsSchema = z.object({
name: z.string(),
maxConcurrent: z.number().optional(),
+13
View File
@@ -1,5 +1,18 @@
# @trigger.dev/express
## 2.0.5
### Patch Changes
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/express",
"version": "2.0.3",
"version": "2.0.5",
"description": "Official Express adapter for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -18,7 +18,7 @@
"./package.json": "./package.json"
},
"devDependencies": {
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.5",
"@trigger.dev/tsconfig": "workspace:*",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.13",
@@ -32,7 +32,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.0.3"
"@trigger.dev/sdk": "workspace:^2.0.5"
},
"dependencies": {
"@remix-run/web-fetch": "^4.3.5",
+4
View File
@@ -1,5 +1,9 @@
# @trigger.dev/integration-kit
## 2.0.5
## 2.0.4
## 2.0.3
## 2.0.2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/integration-kit",
"version": "2.0.3",
"version": "2.0.5",
"description": "Trigger.dev Integration Kit has helpers to make creating integrations easier",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+13
View File
@@ -1,5 +1,18 @@
# @trigger.dev/nextjs
## 2.0.5
### Patch Changes
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
## 2.0.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/nextjs",
"version": "2.0.3",
"version": "2.0.5",
"description": "Trigger.dev Next.js integration",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -32,7 +32,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.5",
"next": ">=12.0.0 <14.0.0"
},
"dependencies": {
+13
View File
@@ -1,5 +1,18 @@
# @trigger.dev/react
## 2.0.5
### Patch Changes
- @trigger.dev/core@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/core@2.0.4
## 2.0.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/react",
"version": "2.0.3",
"version": "2.0.5",
"description": "Trigger.dev React SDK",
"types": "dist/index.d.ts",
"main": "dist/index.cjs",
@@ -26,7 +26,7 @@
},
"dependencies": {
"@tanstack/react-query": "5.0.0-beta.2",
"@trigger.dev/core": "workspace:^2.0.3",
"@trigger.dev/core": "workspace:^2.0.5",
"debug": "^4.3.4",
"zod": "3.21.4"
},
+14
View File
@@ -1,5 +1,19 @@
# @trigger.dev/sdk
## 2.0.5
### Patch Changes
- @trigger.dev/core@2.0.5
## 2.0.4
### Patch Changes
- 96384991: Adding the validate endpoint action to be able to add an endpoint first in the dashboard
- Updated dependencies [96384991]
- @trigger.dev/core@2.0.4
## 2.0.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sdk",
"version": "2.0.3",
"version": "2.0.5",
"description": "trigger.dev Node.JS SDK",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,7 +24,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/core": "workspace:^2.0.3",
"@trigger.dev/core": "workspace:^2.0.5",
"chalk": "^5.2.0",
"cronstrue": "^2.21.0",
"debug": "^4.3.4",
@@ -389,6 +389,15 @@ export class TriggerClient {
},
};
}
case "VALIDATE": {
return {
status: 200,
body: {
ok: true,
endpointId: this.id,
},
};
}
}
return {
+20 -18
View File
@@ -94,6 +94,7 @@ importers:
'@trigger.dev/companyicons': ^1.5.14
'@trigger.dev/core': workspace:*
'@trigger.dev/database': workspace:*
'@trigger.dev/sdk': workspace:2.0.5
'@trigger.dev/tailwind-config': workspace:*
'@types/bcryptjs': ^2.4.2
'@types/compression': ^1.7.2
@@ -218,6 +219,7 @@ importers:
'@trigger.dev/companyicons': 1.5.14_biqbaboplfbrettd7655fr4n2y
'@trigger.dev/core': link:../../packages/core
'@trigger.dev/database': link:../../packages/database
'@trigger.dev/sdk': link:../../packages/trigger-sdk
'@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle
class-variance-authority: 0.5.2_typescript@4.9.4
clsx: 1.2.1
@@ -575,8 +577,8 @@ importers:
'@octokit/types': ^9.2.3
'@octokit/webhooks': ^10.4.0
'@octokit/webhooks-types': ^6.10.0
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/node': '18'
octokit: ^2.0.14
@@ -601,8 +603,8 @@ importers:
integrations/openai:
specifiers:
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/node': '18'
openai: ^3.3.0
@@ -623,8 +625,8 @@ importers:
integrations/plain:
specifiers:
'@team-plain/typescript-sdk': ^2.7.0
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/node': '18'
rimraf: ^3.0.2
@@ -641,8 +643,8 @@ importers:
integrations/resend:
specifiers:
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/node': '18'
resend: ^0.9.1
@@ -661,7 +663,7 @@ importers:
integrations/slack:
specifiers:
'@slack/web-api': ^6.8.1
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/node': '18'
rimraf: ^3.0.2
@@ -679,8 +681,8 @@ importers:
integrations/stripe:
specifiers:
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@types/node': 16.x
rimraf: ^3.0.2
stripe: ^12.14.0
@@ -703,8 +705,8 @@ importers:
integrations/supabase:
specifiers:
'@supabase/supabase-js': ^2.26.0
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@types/node': 18.x
rimraf: ^3.0.2
supabase-management-js: ^0.1.4
@@ -725,8 +727,8 @@ importers:
integrations/typeform:
specifiers:
'@trigger.dev/integration-kit': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/integration-kit': workspace:^2.0.5
'@trigger.dev/sdk': workspace:^2.0.5
'@typeform/api-client': ^1.8.0
'@types/node': 16.x
rimraf: ^3.0.2
@@ -891,7 +893,7 @@ importers:
packages/express:
specifiers:
'@remix-run/web-fetch': ^4.3.5
'@trigger.dev/sdk': workspace:^2.0.3
'@trigger.dev/sdk': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/debug': ^4.1.7
'@types/express': ^4.17.13
@@ -956,7 +958,7 @@ importers:
packages/react:
specifiers:
'@tanstack/react-query': 5.0.0-beta.2
'@trigger.dev/core': workspace:^2.0.3
'@trigger.dev/core': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/debug': ^4.1.7
'@types/react': 18.2.17
@@ -986,7 +988,7 @@ importers:
packages/trigger-sdk:
specifiers:
'@trigger.dev/core': workspace:^2.0.3
'@trigger.dev/core': workspace:^2.0.5
'@trigger.dev/tsconfig': workspace:*
'@types/debug': ^4.1.7
'@types/node': '18'