Merge branch 'main' into v3/worker-attempt-creation

This commit is contained in:
nicktrn
2024-05-24 18:29:55 +01:00
187 changed files with 5736 additions and 2210 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Fixing missing logs when importing client @opentelemetry/api
+7
View File
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"trigger.dev": patch
"@trigger.dev/core": patch
---
v3: Environment variable management API and SDK, along with resolveEnvVars CLI hook
+5
View File
@@ -0,0 +1,5 @@
---
trigger.dev: patch
---
Fix TypeScript inclusion in tsconfig.json for `cli-v3 init`
+7 -1
View File
@@ -55,6 +55,7 @@
"clever-apes-collect",
"clever-carrots-travel",
"clever-donkeys-hunt",
"cool-comics-burn",
"cool-glasses-bake",
"cuddly-feet-approve",
"dry-walls-check",
@@ -63,9 +64,11 @@
"eleven-paws-join",
"famous-boats-tease",
"few-students-share",
"five-toes-destroy",
"funny-swans-destroy",
"gorgeous-gorillas-compete",
"green-bags-wink",
"hot-fishes-retire",
"khaki-apricots-design",
"khaki-poems-lay",
"late-icons-lie",
@@ -109,7 +112,9 @@
"smart-olives-eat",
"spicy-lamps-smoke",
"strange-ghosts-matter",
"strange-sheep-pull",
"strong-lemons-add",
"strong-owls-know",
"stupid-bulldogs-applaud",
"sweet-lizards-press",
"swift-dragons-peel",
@@ -126,6 +131,7 @@
"tiny-elephants-scream",
"tricky-bulldogs-heal",
"tricky-ladybugs-unite",
"two-pumas-wait"
"two-pumas-wait",
"warm-planes-taste"
]
}
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
v2: Better handle recovering from platform communication errors by auto-yielding back to the platform in case of temporary API failures
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
When a v2 run hits the rate limit, reschedule with the reset date
+7
View File
@@ -0,0 +1,7 @@
---
"trigger.dev": patch
---
v3: Prevent legacy-peer-deps=true from breaking deploys
When a global `.npmrc` file includes `legacy-peer-deps=true`, deploys would fail on the `npm ci` step because the package-lock.json wouldn't match the `package.json` file. This is because inside the image build, the `.npmrc` file would not be picked up and so `legacy-peer-deps` would end up being false (which is the default). This change forces the `package-lock.json` file to be created using `legacy-peer-deps=false`
+8
View File
@@ -45,6 +45,14 @@
"cwd": "${workspaceFolder}/references/v3-catalog",
"sourceMaps": true
},
{
"type": "node-terminal",
"request": "launch",
"name": "Debug V3 Management",
"command": "pnpm run management",
"cwd": "${workspaceFolder}/references/v3-catalog",
"sourceMaps": true
},
{
"type": "node",
"request": "attach",
+5
View File
@@ -17,6 +17,7 @@ branch are tagged into a release periodically.
- [Node.js](https://nodejs.org/en) version 20.11.1
- [pnpm package manager](https://pnpm.io/installation) version 8.15.5
- [Docker](https://www.docker.com/get-started/)
- [protobuf](https://github.com/protocolbuffers/protobuf)
### Setup
@@ -102,13 +103,17 @@ First, make sure you are running the webapp according to the instructions above.
4. Build the CLI
```sh
# Build the CLI
pnpm run build --filter trigger.dev
# Make it accessible to `pnpm exec`
pnpm i
```
5. Change into the `<root>/references/v3-catalog` directory and authorize the CLI to the local server:
```sh
cd references/v3-catalog
cp .env.example .env
pnpm exec triggerdev login -a http://localhost:3030
```
+4
View File
@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.861435" y="0.861435" width="30.2771" height="30.2771" rx="15.1386" stroke="#D7D9DD" stroke-width="1.72287"/>
<path d="M14.9669 12.642L11.6417 21.7732H9.8155L6.49036 12.642H8.04094L10.7286 20.2571L13.4163 12.642H14.9669ZM20.534 14.5544C21.6884 14.5716 23.0666 14.0375 23.0666 12.4525C23.0666 11.3843 22.1018 10.7124 20.534 10.7124C19.2591 10.7124 18.3632 11.3326 18.2082 12.3146L16.6576 12.2113C16.8815 10.4884 18.4838 9.26516 20.534 9.26516C22.946 9.26516 24.6345 10.5745 24.6345 12.4525C24.6345 13.8135 23.8592 14.7267 22.3775 15.1574C24.0831 15.657 25.0479 16.8286 25.0479 18.4136C25.0479 20.55 23.1872 22.0489 20.534 22.0489C18.0876 22.0489 16.313 20.6361 16.2096 18.6203L17.743 18.517C17.8808 20.0159 19.3108 20.6016 20.534 20.6016C21.9812 20.6016 23.4974 19.947 23.4974 18.293C23.4974 16.6563 21.9812 15.9499 20.534 15.9844L19.5865 16.0016V14.5371L20.534 14.5544Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 999 B

+14 -5
View File
@@ -1,14 +1,23 @@
import { Clipboard, ClipboardCheck } from "lucide-react";
import type { Language, PrismTheme } from "prism-react-renderer";
import Highlight, { defaultProps } from "prism-react-renderer";
import { Highlight, Prism } from "prism-react-renderer";
import { forwardRef, useCallback, useState } from "react";
import { cn } from "~/utils/cn";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
import { Paragraph } from "../primitives/Paragraph";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
//it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting
async function setup() {
(typeof global !== "undefined" ? global : window).Prism = Prism;
//@ts-ignore
await import("prismjs/components/prism-json");
//@ts-ignore
await import("prismjs/components/prism-typescript");
}
setup();
type CodeBlockProps = {
/** Code which will be highlighted */
code: string;
@@ -238,7 +247,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
)}
{shouldHighlight ? (
<Highlight {...defaultProps} theme={theme} code={code} language={language}>
<Highlight theme={theme} code={code} language={language}>
{({
className: inheritedClassName,
style: inheritedStyle,
@@ -283,7 +292,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
return (
<div
key={lineProps.key}
key={lineNumber}
{...lineProps}
className={cn(
"flex w-full justify-start transition-opacity duration-500",
@@ -312,7 +321,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
const tokenProps = getTokenProps({ token, key });
return (
<span
key={tokenProps.key}
key={key}
{...tokenProps}
style={{
color: tokenProps?.style?.color as string,
@@ -7,14 +7,15 @@ import {
XMarkIcon,
} from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import {
BulkActionType,
import type {
RuntimeEnvironment,
TaskRunStatus,
TaskTriggerSource,
TaskRunStatus,
BulkActionType,
} from "@trigger.dev/database";
import { ListFilterIcon } from "lucide-react";
import { ReactNode, startTransition, useCallback, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { startTransition, useCallback, useMemo, useState } from "react";
import { z } from "zod";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
@@ -43,6 +44,7 @@ import { Button } from "../../primitives/Buttons";
import {
TaskRunStatusCombo,
allTaskRunStatuses,
filterableTaskRunStatuses,
descriptionForTaskRunStatus,
runStatusTitle,
} from "./TaskRunStatus";
@@ -50,7 +52,7 @@ import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
import { DateTime } from "~/components/primitives/DateTime";
import { BulkActionStatusCombo } from "./BulkAction";
export const TaskAttemptStatus = z.nativeEnum(TaskRunStatus);
export const TaskAttemptStatus = z.enum(allTaskRunStatuses);
export const TaskRunListSearchFilters = z.object({
cursor: z.string().optional(),
@@ -271,7 +273,7 @@ function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: Men
);
}
const statuses = allTaskRunStatuses.map((status) => ({
const statuses = filterableTaskRunStatuses.map((status) => ({
title: runStatusTitle(status),
value: status,
}));
@@ -7,7 +7,7 @@ import {
XCircleIcon,
} from "@heroicons/react/20/solid";
import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger.dev/database";
import { TaskRunAttemptStatus } from "@trigger.dev/database";
import { TaskRunAttemptStatus } from "~/database-types";
import assertNever from "assert-never";
import { SnowflakeIcon } from "lucide-react";
import { Spinner } from "~/components/primitives/Spinner";
@@ -17,7 +17,7 @@ export const allTaskRunAttemptStatuses = Object.values(
TaskRunAttemptStatus
) as TaskRunAttemptStatusType[];
export type ExtendedTaskAttemptStatus = (typeof allTaskRunAttemptStatuses)[number] | "ENQUEUED";
export type ExtendedTaskAttemptStatus = TaskRunAttemptStatusType | "ENQUEUED";
export function TaskRunAttemptStatusCombo({
status,
@@ -25,9 +25,24 @@ export const allTaskRunStatuses = [
"CANCELED",
"COMPLETED_WITH_ERRORS",
"CRASHED",
"PAUSED",
"INTERRUPTED",
"SYSTEM_FAILURE",
] as TaskRunStatus[];
] as const satisfies Readonly<Array<TaskRunStatus>>;
export const filterableTaskRunStatuses = [
"WAITING_FOR_DEPLOY",
"PENDING",
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
"CRASHED",
"INTERRUPTED",
"SYSTEM_FAILURE",
] as const satisfies Readonly<Array<TaskRunStatus>>;
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
PENDING: "Task is waiting to be executed",
+68
View File
@@ -0,0 +1,68 @@
// There's a weird issue with importing values from the prisma client
// when using Remix Vite + pnpm + prisma
// As long as they're only used as types it's ok
// Import types here and validate hardcoded enums
import type {
BatchTaskRunItemStatus as BatchTaskRunItemStatusType,
TaskRunAttemptStatus as TaskRunAttemptStatusType,
TaskRunStatus as TaskRunStatusType,
JobRunStatus as JobRunStatusType,
RuntimeEnvironmentType as RuntimeEnvironmentTypeType,
} from "@trigger.dev/database";
export const BatchTaskRunItemStatus = {
PENDING: "PENDING",
FAILED: "FAILED",
CANCELED: "CANCELED",
COMPLETED: "COMPLETED",
} as const satisfies Record<BatchTaskRunItemStatusType, BatchTaskRunItemStatusType>;
export const TaskRunAttemptStatus = {
PENDING: "PENDING",
EXECUTING: "EXECUTING",
PAUSED: "PAUSED",
FAILED: "FAILED",
CANCELED: "CANCELED",
COMPLETED: "COMPLETED",
} as const satisfies Record<TaskRunAttemptStatusType, TaskRunAttemptStatusType>;
export const TaskRunStatus = {
PENDING: "PENDING",
WAITING_FOR_DEPLOY: "WAITING_FOR_DEPLOY",
EXECUTING: "EXECUTING",
WAITING_TO_RESUME: "WAITING_TO_RESUME",
RETRYING_AFTER_FAILURE: "RETRYING_AFTER_FAILURE",
PAUSED: "PAUSED",
CANCELED: "CANCELED",
INTERRUPTED: "INTERRUPTED",
COMPLETED_SUCCESSFULLY: "COMPLETED_SUCCESSFULLY",
COMPLETED_WITH_ERRORS: "COMPLETED_WITH_ERRORS",
SYSTEM_FAILURE: "SYSTEM_FAILURE",
CRASHED: "CRASHED",
} as const satisfies Record<TaskRunStatusType, TaskRunStatusType>;
export const JobRunStatus = {
PENDING: "PENDING",
QUEUED: "QUEUED",
WAITING_ON_CONNECTIONS: "WAITING_ON_CONNECTIONS",
PREPROCESSING: "PREPROCESSING",
STARTED: "STARTED",
EXECUTING: "EXECUTING",
WAITING_TO_CONTINUE: "WAITING_TO_CONTINUE",
WAITING_TO_EXECUTE: "WAITING_TO_EXECUTE",
SUCCESS: "SUCCESS",
FAILURE: "FAILURE",
TIMED_OUT: "TIMED_OUT",
ABORTED: "ABORTED",
CANCELED: "CANCELED",
UNRESOLVED_AUTH: "UNRESOLVED_AUTH",
INVALID_PAYLOAD: "INVALID_PAYLOAD",
} as const satisfies Record<JobRunStatusType, JobRunStatusType>;
export const RuntimeEnvironmentType = {
PRODUCTION: "PRODUCTION",
STAGING: "STAGING",
DEVELOPMENT: "DEVELOPMENT",
PREVIEW: "PREVIEW",
} as const satisfies Record<RuntimeEnvironmentTypeType, RuntimeEnvironmentTypeType>;
+3 -1
View File
@@ -70,7 +70,9 @@ export { Prisma };
export const prisma = singleton("prisma", getClient);
export const $replica: Omit<PrismaClient, "$transaction"> = singleton(
export type PrismaReplicaClient = Omit<PrismaClient, "$transaction">;
export const $replica: PrismaReplicaClient = singleton(
"replica",
() => getReplicaClient() ?? prisma
);
+1 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { SecretStoreOptionsSchema } from "./services/secrets/secretStore.server";
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
import { isValidRegex } from "./utils/regex";
import { isValidDatabaseUrl } from "./utils/db";
+2 -1
View File
@@ -1,6 +1,7 @@
import { RuntimeEnvironmentType, type RuntimeEnvironment } from "@trigger.dev/database";
import type { RuntimeEnvironment } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { customAlphabet } from "nanoid";
import { RuntimeEnvironmentType } from "~/database-types";
const apiKeyId = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
+2 -15
View File
@@ -21,16 +21,12 @@ export async function createOrganization(
{
title,
userId,
projectName,
companySize,
projectVersion,
}: Pick<Organization, "title" | "companySize"> & {
userId: User["id"];
projectName: string;
projectVersion: "v2" | "v3";
},
attemptCount = 0
): Promise<Organization & { projects: Project[] }> {
): Promise<Organization> {
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
throw new Error("Organization could not be created.");
}
@@ -50,9 +46,7 @@ export async function createOrganization(
{
title,
userId,
projectName,
companySize,
projectVersion,
},
attemptCount + 1
);
@@ -76,14 +70,7 @@ export async function createOrganization(
},
});
const project = await createProject({
organizationSlug: organization.slug,
name: projectName,
userId,
version: projectVersion,
});
return { ...organization, projects: [project] };
return { ...organization };
}
export async function createEnvironment(
+12 -8
View File
@@ -1,17 +1,20 @@
import {
TaskRunError,
import type {
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
TaskRunSuccessfulExecutionResult,
} from "@trigger.dev/core/v3";
import {
BatchTaskRunItemStatus,
import { TaskRunError } from "@trigger.dev/core/v3";
import type {
TaskRun,
TaskRunAttempt,
TaskRunAttemptStatus,
TaskRunStatus,
TaskRunAttemptStatus as TaskRunAttemptStatusType,
TaskRunStatus as TaskRunStatusType,
BatchTaskRunItemStatus as BatchTaskRunItemStatusType,
} from "@trigger.dev/database";
import { assertNever } from "assert-never";
import { BatchTaskRunItemStatus, TaskRunAttemptStatus, TaskRunStatus } from "~/database-types";
import { logger } from "~/services/logger.server";
const SUCCESSFUL_STATUSES = [TaskRunStatus.COMPLETED_SUCCESSFULLY];
@@ -104,7 +107,9 @@ export function executionResultForTaskRun(
}
}
export function batchTaskRunItemStatusForRunStatus(status: TaskRunStatus): BatchTaskRunItemStatus {
export function batchTaskRunItemStatusForRunStatus(
status: TaskRunStatusType
): BatchTaskRunItemStatusType {
switch (status) {
case TaskRunStatus.COMPLETED_SUCCESSFULLY:
return BatchTaskRunItemStatus.COMPLETED;
@@ -113,7 +118,6 @@ export function batchTaskRunItemStatusForRunStatus(status: TaskRunStatus): Batch
case TaskRunStatus.COMPLETED_WITH_ERRORS:
case TaskRunStatus.SYSTEM_FAILURE:
case TaskRunStatus.CRASHED:
case TaskRunStatus.COMPLETED_WITH_ERRORS:
return BatchTaskRunItemStatus.FAILED;
case TaskRunStatus.PENDING:
case TaskRunStatus.WAITING_FOR_DEPLOY:
@@ -1,8 +1,8 @@
import { PrismaClient } from "@trigger.dev/database";
import { redirect } from "remix-typedjson";
import { prisma } from "~/db.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import {
clearCurrentProjectId,
commitCurrentProjectSession,
getCurrentProjectId,
setCurrentProjectId,
@@ -10,8 +10,6 @@ import {
import { logger } from "~/services/logger.server";
import { newProjectPath } from "~/utils/pathBuilder";
import { ProjectPresenter } from "./ProjectPresenter.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import { match } from "assert";
export class OrganizationsPresenter {
#prismaClient: PrismaClient;
@@ -64,6 +62,10 @@ export class OrganizationsPresenter {
);
}
if (project.organizationId !== organization.id) {
throw redirect(newProjectPath({ slug: organizationSlug }), request);
}
return { organizations, organization, project };
}
@@ -87,7 +87,7 @@ export class EnvironmentVariablesPresenter {
);
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
const variables = await repository.getProject(project.id, userId);
const variables = await repository.getProject(project.id);
return {
environmentVariables: environmentVariables.map((environmentVariable) => {
@@ -1,29 +1,26 @@
import { prettyPrintPacket } from "@trigger.dev/core/v3";
import { PrismaClient, prisma } from "~/db.server";
import { eventRepository } from "~/v3/eventRepository.server";
import { BasePresenter } from "./basePresenter.server";
type Result = Awaited<ReturnType<SpanPresenter["call"]>>;
export type Span = NonNullable<Result>["event"];
export class SpanPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
export class SpanPresenter extends BasePresenter {
public async call({
userId,
projectSlug,
organizationSlug,
spanId,
runFriendlyId,
}: {
userId: string;
projectSlug: string;
organizationSlug: string;
spanId: string;
runFriendlyId: string;
}) {
const project = await this.#prismaClient.project.findUnique({
const project = await this._replica.project.findUnique({
where: {
slug: projectSlug,
},
@@ -33,7 +30,20 @@ export class SpanPresenter {
throw new Error("Project not found");
}
const span = await eventRepository.getSpan(spanId);
const run = await this._prisma.taskRun.findFirst({
select: {
traceId: true,
},
where: {
friendlyId: runFriendlyId,
},
});
if (!run) {
return;
}
const span = await eventRepository.getSpan(spanId, run.traceId);
if (!span) {
return;
@@ -1,18 +1,19 @@
import {
Prisma,
import type {
RuntimeEnvironmentType,
TaskRunStatus,
TaskTriggerSource,
TaskRunStatus as TaskRunStatusType,
} from "@trigger.dev/database";
import { Prisma } from "@trigger.dev/database";
import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { sqlDatabaseSchema } from "~/db.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
import { User } from "~/models/user.server";
import type { User } from "~/models/user.server";
import { sortEnvironments } from "~/utils/environmentSort";
import { logger } from "~/services/logger.server";
import { BasePresenter } from "./basePresenter.server";
import { TaskRunStatus } from "~/database-types";
export type Task = {
slug: string;
@@ -110,6 +111,14 @@ export class TaskListPresenter extends BasePresenter {
acc.push(existingTask);
}
//favour newer tasks
if (task.createdAt > existingTask.createdAt) {
existingTask.createdAt = task.createdAt;
existingTask.exportName = task.exportName;
existingTask.filePath = task.filePath;
existingTask.triggerSource = task.triggerSource;
}
existingTask.environments.push(displayableEnvironments(environment, userId));
//order the environments
@@ -150,7 +159,7 @@ export class TaskListPresenter extends BasePresenter {
const activity = await this._replica.$queryRaw<
{
taskIdentifier: string;
status: TaskRunStatus;
status: TaskRunStatusType;
day: Date;
count: BigInt;
}[]
@@ -193,7 +202,7 @@ export class TaskListPresenter extends BasePresenter {
existingTask.push({
day: day.toISOString(),
[TaskRunStatus.COMPLETED_SUCCESSFULLY]: 0,
} as { day: string } & Record<TaskRunStatus, number>);
} as { day: string } & Record<TaskRunStatusType, number>);
}
acc[a.taskIdentifier] = existingTask;
@@ -214,7 +223,7 @@ export class TaskListPresenter extends BasePresenter {
day[a.status] = Number(a.count);
return acc;
}, {} as Record<string, ({ day: string } & Record<TaskRunStatus, number>)[]>);
}, {} as Record<string, ({ day: string } & Record<TaskRunStatusType, number>)[]>);
}
async #getRunningStats(tasks: string[], projectId: string) {
@@ -225,7 +234,7 @@ export class TaskListPresenter extends BasePresenter {
const statuses = await this._replica.$queryRaw<
{
taskIdentifier: string;
status: TaskRunStatus;
status: TaskRunStatusType;
count: BigInt;
}[]
>`
+20 -1
View File
@@ -1,8 +1,9 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { prisma } from "~/db.server";
import { getUsersInvites } from "~/models/member.server";
import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server";
import { requireUser } from "~/services/session.server";
import { invitesPath, newOrganizationPath, projectPath } from "~/utils/pathBuilder";
import { invitesPath, newOrganizationPath, newProjectPath, projectPath } from "~/utils/pathBuilder";
//this loader chooses the best project to redirect you to, ideally based on the cookie
export const loader = async ({ request }: LoaderFunctionArgs) => {
@@ -20,6 +21,24 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
//redirect them to the most appropriate project
return redirect(projectPath(organization, project));
} catch (e) {
const organization = await prisma.organization.findFirst({
where: {
members: {
some: {
userId: user.id,
},
},
deletedAt: null,
},
orderBy: {
createdAt: "desc",
},
});
if (organization) {
return redirect(newProjectPath(organization));
}
//this should only happen if the user has no projects, and no invites
return redirect(newOrganizationPath());
}
@@ -70,7 +70,7 @@ const Variable = z.object({
type Variable = z.infer<typeof Variable>;
const schema = z.object({
overwrite: z.preprocess((i) => {
override: z.preprocess((i) => {
if (i === "true") return true;
if (i === "false") return false;
return;
@@ -115,6 +115,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
const project = await prisma.project.findUnique({
where: {
slug: params.projectParam,
organization: {
members: {
some: {
userId,
},
},
},
},
select: {
id: true,
@@ -126,7 +133,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
}
const repository = new EnvironmentVariablesRepository(prisma);
const result = await repository.create(project.id, userId, submission.value);
const result = await repository.create(project.id, submission.value);
if (!result.success) {
if (result.variableErrors) {
@@ -249,7 +256,7 @@ export default function Page() {
type="submit"
variant="primary/small"
disabled={isLoading}
name="overwrite"
name="override"
value="false"
>
{isLoading ? "Saving" : "Save"}
@@ -257,10 +264,10 @@ export default function Page() {
<Button
variant="secondary/small"
disabled={isLoading}
name="overwrite"
name="override"
value="true"
>
{isLoading ? "Overwriting" : "Overwrite"}
{isLoading ? "Overriding" : "Override"}
</Button>
</div>
}
@@ -106,6 +106,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
const project = await prisma.project.findUnique({
where: {
slug: params.projectParam,
organization: {
members: {
some: {
userId,
},
},
},
},
select: {
id: true,
@@ -119,7 +126,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
switch (submission.value.action) {
case "edit": {
const repository = new EnvironmentVariablesRepository(prisma);
const result = await repository.edit(project.id, userId, submission.value);
const result = await repository.edit(project.id, submission.value);
if (!result.success) {
submission.error.key = result.error;
@@ -138,7 +145,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
}
case "delete": {
const repository = new EnvironmentVariablesRepository(prisma);
const result = await repository.delete(project.id, userId, submission.value);
const result = await repository.delete(project.id, submission.value);
if (!result.success) {
submission.error.key = result.error;
@@ -334,6 +341,7 @@ function EditEnvironmentVariablePanel({
name={`values[${index}].value`}
placeholder="Not set"
defaultValue={value}
type="password"
/>
</Fragment>
);
@@ -16,13 +16,17 @@ import { FormTitle } from "~/components/primitives/FormTitle";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Select, SelectItem } from "~/components/primitives/Select";
import { TextLink } from "~/components/primitives/TextLink";
import { prisma } from "~/db.server";
import { useFeatures } from "~/hooks/useFeatures";
import { useUser } from "~/hooks/useUser";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { createProject } from "~/models/project.server";
import { requireUserId } from "~/services/session.server";
import { OrganizationParamsSchema, organizationPath, projectPath } from "~/utils/pathBuilder";
import { RequestV3Access } from "../resources.orgs.$organizationSlug.v3-access";
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
@@ -34,10 +38,14 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
id: true,
title: true,
v3Enabled: true,
v2Enabled: true,
hasRequestedV3: true,
_count: {
select: {
projects: {
where: { deletedAt: null },
where: {
deletedAt: null,
},
},
},
},
@@ -57,6 +65,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
slug: organizationSlug,
projectsCount: organization._count.projects,
v3Enabled: organization.v3Enabled,
v2Enabled: organization.v2Enabled,
hasRequestedV3: organization.hasRequestedV3,
},
defaultVersion: url.searchParams.get("version") ?? "v2",
});
@@ -98,11 +108,23 @@ export const action: ActionFunction = async ({ request, params }) => {
};
export default function NewOrganizationPage() {
const { organization, defaultVersion } = useTypedLoaderData<typeof loader>();
const { organization } = useTypedLoaderData<typeof loader>();
const lastSubmission = useActionData();
const { v3Enabled } = useFeatures();
const { v3Enabled, isManagedCloud } = useFeatures();
const canCreateV3Projects = organization.v3Enabled && v3Enabled;
const canCreateV2Projects = organization.v2Enabled || !isManagedCloud;
const canCreateProjects = canCreateV2Projects || canCreateV3Projects;
if (!canCreateProjects) {
return (
<RequestV3Access
hasRequestedV3={organization.hasRequestedV3}
organizationSlug={organization.slug}
projectsCount={organization.projectsCount}
/>
);
}
const [form, { projectName, projectVersion }] = useForm({
id: "create-project",
@@ -119,7 +141,7 @@ export default function NewOrganizationPage() {
<FormTitle
LeadingIcon="folder"
title="Create a new project"
description={`This will create a new project in your "${organization.title}" organization. `}
description={`This will create a new project in your "${organization.title}" organization.`}
/>
<Form method="post" {...form.props}>
{organization.projectsCount === 0 && (
@@ -138,7 +160,7 @@ export default function NewOrganizationPage() {
/>
<FormError id={projectName.errorId}>{projectName.error}</FormError>
</InputGroup>
{canCreateV3Projects ? (
{canCreateV2Projects && canCreateV3Projects ? (
<InputGroup>
<Label htmlFor={projectVersion.id}>Project version</Label>
<Select
@@ -161,8 +183,16 @@ export default function NewOrganizationPage() {
</Select>
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
</InputGroup>
) : canCreateV3Projects ? (
<>
<Callout variant="info">This will be a v3 project</Callout>
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v3"} />
</>
) : (
<input {...conform.input(projectVersion, { type: "hidden" })} value="v2" />
<>
<Callout variant="info">This will be a v2 project</Callout>
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v2"} />
</>
)}
<FormButtons
confirmButton={
+4 -67
View File
@@ -17,19 +17,14 @@ import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { RadioGroupItem } from "~/components/primitives/RadioButton";
import { Select, SelectItem } from "~/components/primitives/Select";
import { featuresForRequest } from "~/features.server";
import { useFeatures } from "~/hooks/useFeatures";
import { createOrganization } from "~/models/organization.server";
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
import { commitCurrentProjectSession, setCurrentProjectId } from "~/services/currentProject.server";
import { requireUserId } from "~/services/session.server";
import { projectPath, rootPath, selectPlanPath } from "~/utils/pathBuilder";
import { organizationPath, rootPath } from "~/utils/pathBuilder";
const schema = z.object({
orgName: z.string().min(3).max(50),
projectName: z.string().min(3).max(50),
projectVersion: z.enum(["v2", "v3"]),
companySize: z.string().optional(),
});
@@ -57,29 +52,10 @@ export const action: ActionFunction = async ({ request }) => {
const organization = await createOrganization({
title: submission.value.orgName,
userId,
projectName: submission.value.projectName,
companySize: submission.value.companySize ?? null,
projectVersion: submission.value.projectVersion,
});
const project = organization.projects[0];
const session = await setCurrentProjectId(project.id, request);
const { isManagedCloud } = featuresForRequest(request);
const headers = {
"Set-Cookie": await commitCurrentProjectSession(session),
};
if (isManagedCloud && submission.value.projectVersion === "v2") {
return redirect(selectPlanPath(organization), {
headers,
});
}
return redirect(projectPath(organization, project), {
headers,
});
return redirect(organizationPath(organization));
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
@@ -91,10 +67,7 @@ export default function NewOrganizationPage() {
const { isManagedCloud } = useFeatures();
const navigation = useNavigation();
//this is temporary whilst v3 is invite-only. Switch to the useFeatures value when v3 is generally available.
const v3Enabled = false;
const [form, { orgName, projectName, projectVersion }] = useForm({
const [form, { orgName }] = useForm({
id: "create-organization",
// TODO: type this
lastSubmission: lastSubmission as any,
@@ -123,45 +96,9 @@ export default function NewOrganizationPage() {
<Hint>E.g. your company name or your workspace name.</Hint>
<FormError id={orgName.errorId}>{orgName.error}</FormError>
</InputGroup>
<InputGroup>
<Label htmlFor={projectName.id}>Project name</Label>
<Input
{...conform.input(projectName, { type: "text" })}
placeholder="Your Project name"
icon="folder"
/>
<Hint>Your Jobs will live inside this Project.</Hint>
<FormError id={projectName.errorId}>{projectName.error}</FormError>
</InputGroup>
{v3Enabled ? (
<InputGroup>
<Label htmlFor={projectVersion.id}>Project version</Label>
<Select
{...conform.select(projectVersion)}
defaultValue={undefined}
variant="tertiary/medium"
placeholder="Select version"
dropdownIcon
text={(value) => {
switch (value) {
case "v2":
return "Version 2";
case "v3":
return "Version 3";
}
}}
>
<SelectItem value="v2">Version 2</SelectItem>
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
</Select>
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
</InputGroup>
) : (
<input {...conform.input(projectVersion, { type: "hidden" })} value="v2" />
)}
{isManagedCloud && (
<InputGroup>
<Label htmlFor={projectName.id}>Number of employees</Label>
<Label htmlFor={"companySize"}>Number of employees</Label>
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
<RadioGroupItem
id="employees-1-5"
@@ -0,0 +1,49 @@
import { CreateExternalConnectionBody } from "@trigger.dev/core";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
export class CreateExternalConnectionService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
accountIdentifier: string,
clientSlug: string,
environment: AuthenticatedEnvironment,
payload: CreateExternalConnectionBody
) {
const externalAccount = await this.#prismaClient.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: accountIdentifier,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: accountIdentifier,
},
update: {},
});
const integration = await this.#prismaClient.integration.findUniqueOrThrow({
where: {
organizationId_slug: {
organizationId: environment.organizationId,
slug: clientSlug,
},
},
});
return await integrationAuthRepository.createConnectionFromToken({
externalAccount: externalAccount,
integration,
token: payload,
});
}
}
@@ -1,15 +1,10 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
CreateExternalConnectionBody,
CreateExternalConnectionBodySchema,
ErrorWithStackSchema,
} from "@trigger.dev/core";
import { CreateExternalConnectionBodySchema, ErrorWithStackSchema } from "@trigger.dev/core";
import { z } from "zod";
import { generateErrorMessage } from "zod-error";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { AuthenticatedEnvironment, authenticateApiRequest } from "~/services/apiAuth.server";
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CreateExternalConnectionService } from "./CreateExternalConnectionService.server";
const ParamsSchema = z.object({
accountId: z.string(),
@@ -67,48 +62,3 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ message: parsedError.data.message }, { status: 500 });
}
}
class CreateExternalConnectionService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
accountIdentifier: string,
clientSlug: string,
environment: AuthenticatedEnvironment,
payload: CreateExternalConnectionBody
) {
const externalAccount = await this.#prismaClient.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: accountIdentifier,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: accountIdentifier,
},
update: {},
});
const integration = await this.#prismaClient.integration.findUniqueOrThrow({
where: {
organizationId_slug: {
organizationId: environment.organizationId,
slug: clientSlug,
},
},
});
return await integrationAuthRepository.createConnectionFromToken({
externalAccount: externalAccount,
integration,
token: payload,
});
}
}
@@ -1,73 +1,10 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import type { PrismaClient } from "~/db.server";
import { $transaction, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerQueue } from "~/services/worker.server";
import { safeJsonParse } from "~/utils/json";
const ParamsSchema = z.object({
environmentId: z.string(),
endpointSlug: z.string(),
indexHookIdentifier: z.string(),
});
export async function loader({ params }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return {
status: 400,
json: {
error: "Invalid params",
},
};
}
const { environmentId, endpointSlug, indexHookIdentifier } = parsedParams.data;
const service = new TriggerEndpointIndexHookService();
await service.call({
environmentId,
endpointSlug,
indexHookIdentifier,
});
return json({
ok: true,
});
}
export async function action({ request, params }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return {
status: 400,
json: {
error: "Invalid params",
},
};
}
const { environmentId, endpointSlug, indexHookIdentifier } = parsedParams.data;
const body = await request.text();
const service = new TriggerEndpointIndexHookService();
await service.call({
environmentId,
endpointSlug,
indexHookIdentifier,
body: body ? safeJsonParse(body) : undefined,
});
return json({
ok: true,
});
}
import { RuntimeEnvironmentType } from "~/database-types";
import type { ParamsSchema } from "./route";
type TriggerEndpointDeployHookOptions = z.infer<typeof ParamsSchema> & {
body?: any;
@@ -0,0 +1,67 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { safeJsonParse } from "~/utils/json";
import { TriggerEndpointIndexHookService } from "./TriggerEndpointIndexHookService.server";
export const ParamsSchema = z.object({
environmentId: z.string(),
endpointSlug: z.string(),
indexHookIdentifier: z.string(),
});
export async function loader({ params }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return {
status: 400,
json: {
error: "Invalid params",
},
};
}
const { environmentId, endpointSlug, indexHookIdentifier } = parsedParams.data;
const service = new TriggerEndpointIndexHookService();
await service.call({
environmentId,
endpointSlug,
indexHookIdentifier,
});
return json({
ok: true,
});
}
export async function action({ request, params }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return {
status: 400,
json: {
error: "Invalid params",
},
};
}
const { environmentId, endpointSlug, indexHookIdentifier } = parsedParams.data;
const body = await request.text();
const service = new TriggerEndpointIndexHookService();
await service.call({
environmentId,
endpointSlug,
indexHookIdentifier,
body: body ? safeJsonParse(body) : undefined,
});
return json({
ok: true,
});
}
@@ -0,0 +1,137 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { UpdateEnvironmentVariableRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import {
authenticateProjectApiKeyOrPersonalAccessToken,
authenticatedEnvironmentForAuthentication,
} from "~/services/apiAuth.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
const ParamsSchema = z.object({
projectRef: z.string(),
slug: z.string(),
name: z.string(),
});
export async function action({ params, request }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);
// Find the environment variable
const variable = await prisma.environmentVariable.findFirst({
where: {
key: parsedParams.data.name,
projectId: environment.project.id,
},
});
if (!variable) {
return json({ error: "Environment variable not found" }, { status: 404 });
}
const repository = new EnvironmentVariablesRepository();
switch (request.method.toUpperCase()) {
case "DELETE": {
const result = await repository.deleteValue(environment.project.id, {
id: variable.id,
environmentId: environment.id,
});
if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error }, { status: 400 });
}
}
case "PUT":
case "POST": {
const jsonBody = await request.json();
const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody);
if (!body.success) {
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
}
const result = await repository.edit(environment.project.id, {
values: [
{
value: body.data.value,
environmentId: environment.id,
},
],
id: variable.id,
keepEmptyValues: true,
});
if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error }, { status: 400 });
}
}
}
}
export async function loader({ params, request }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);
// Find the environment variable
const variable = await prisma.environmentVariable.findFirst({
where: {
key: parsedParams.data.name,
projectId: environment.project.id,
},
});
if (!variable) {
return json({ error: "Environment variable not found" }, { status: 404 });
}
const repository = new EnvironmentVariablesRepository();
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
const environmentVariable = variables.find((v) => v.key === parsedParams.data.name);
if (!environmentVariable) {
return json({ error: "Environment variable not found" }, { status: 404 });
}
return json({
value: environmentVariable.value,
});
}
@@ -0,0 +1,84 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { ImportEnvironmentVariablesRequestBody } from "@trigger.dev/core/v3";
import { parse } from "dotenv";
import { z } from "zod";
import {
authenticateProjectApiKeyOrPersonalAccessToken,
authenticatedEnvironmentForAuthentication,
} from "~/services/apiAuth.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
const ParamsSchema = z.object({
projectRef: z.string(),
slug: z.string(),
});
export async function action({ params, request }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);
const repository = new EnvironmentVariablesRepository();
const body = await parseImportBody(request);
const result = await repository.create(environment.project.id, {
override: typeof body.override === "boolean" ? body.override : false,
environmentIds: [environment.id],
variables: Object.entries(body.variables).map(([key, value]) => ({
key,
value,
})),
});
if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error, variableErrors: result.variableErrors }, { status: 400 });
}
}
async function parseImportBody(request: Request): Promise<ImportEnvironmentVariablesRequestBody> {
const contentType = request.headers.get("content-type") ?? "application/json";
if (contentType.includes("multipart/form-data")) {
const formData = await request.formData();
const file = formData.get("variables");
const override = formData.get("override") === "true";
if (file instanceof File) {
const buffer = await file.arrayBuffer();
const variables = parse(Buffer.from(buffer));
return { variables, override };
} else {
throw json({ error: "Invalid file" }, { status: 400 });
}
} else {
const rawBody = await request.json();
const body = ImportEnvironmentVariablesRequestBody.safeParse(rawBody);
if (!body.success) {
throw json({ error: "Invalid body" }, { status: 400 });
}
return body.data;
}
}
@@ -0,0 +1,86 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { CreateEnvironmentVariableRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import {
authenticateProjectApiKeyOrPersonalAccessToken,
authenticatedEnvironmentForAuthentication,
} from "~/services/apiAuth.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
const ParamsSchema = z.object({
projectRef: z.string(),
slug: z.string(),
});
export async function action({ params, request }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);
const jsonBody = await request.json();
const body = CreateEnvironmentVariableRequestBody.safeParse(jsonBody);
if (!body.success) {
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
}
const repository = new EnvironmentVariablesRepository();
const result = await repository.create(environment.project.id, {
override: true,
environmentIds: [environment.id],
variables: [
{
key: body.data.name,
value: body.data.value,
},
],
});
if (result.success) {
return json({ success: true });
} else {
return json({ error: result.error, variableErrors: result.variableErrors }, { status: 400 });
}
}
export async function loader({ params, request }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}
const authenticationResult = await authenticateProjectApiKeyOrPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
parsedParams.data.projectRef,
parsedParams.data.slug
);
const repository = new EnvironmentVariablesRepository();
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
return json(variables.map((variable) => ({ name: variable.key, value: variable.value })));
}
@@ -0,0 +1,20 @@
import type { LogMessage } from "@trigger.dev/core";
import type { PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
export class CreateRunLogService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(environment: AuthenticatedEnvironment, runId: string, logMessage: LogMessage) {
// @ts-ignore
logger.debug(logMessage.message, logMessage.data ?? {});
return logMessage;
}
}
@@ -1,13 +1,9 @@
import type { Organization, RuntimeEnvironment } from "@trigger.dev/database";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import type { LogMessage } from "@trigger.dev/core";
import { LogMessageSchema } from "@trigger.dev/core";
import { z } from "zod";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { authenticateApiRequest, AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CreateRunLogService } from "./CreateRunLogService.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -53,18 +49,3 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class CreateRunLogService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(environment: AuthenticatedEnvironment, runId: string, logMessage: LogMessage) {
// @ts-ignore
logger.debug(logMessage.message, logMessage.data ?? {});
return logMessage;
}
}
@@ -0,0 +1,71 @@
import {
StatusUpdate,
StatusHistory,
StatusHistorySchema,
StatusUpdateState,
StatusUpdateData,
} from "@trigger.dev/core";
import { PrismaClient } from "@trigger.dev/database";
import { prisma, $transaction } from "~/db.server";
export class SetStatusService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(runId: string, id: string, status: StatusUpdate) {
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
const existingStatus = await tx.jobRunStatusRecord.findUnique({
where: {
runId_key: {
runId,
key: id,
},
},
});
const history: StatusHistory = [];
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
if (historyResult.success) {
history.push(...historyResult.data);
}
if (existingStatus) {
history.push({
label: existingStatus.label,
state: (existingStatus.state ?? undefined) as StatusUpdateState,
data: (existingStatus.data ?? undefined) as StatusUpdateData,
});
}
const updatedStatus = await tx.jobRunStatusRecord.upsert({
where: {
runId_key: {
runId,
key: id,
},
},
create: {
key: id,
runId,
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
label: status.label ?? id,
state: status.state,
data: status.data as any,
history: [],
},
update: {
label: status.label,
state: status.state,
data: status.data as any,
history: history as any[],
},
});
return updatedStatus;
});
return statusRecord;
}
}
@@ -1,18 +1,10 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
JobRunStatusRecordSchema,
StatusHistory,
StatusHistorySchema,
StatusUpdate,
StatusUpdateData,
StatusUpdateSchema,
StatusUpdateState,
} from "@trigger.dev/core";
import { JobRunStatusRecordSchema, StatusUpdateSchema } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { SetStatusService } from "./SetStatusService.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -80,65 +72,3 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class SetStatusService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(runId: string, id: string, status: StatusUpdate) {
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
const existingStatus = await tx.jobRunStatusRecord.findUnique({
where: {
runId_key: {
runId,
key: id,
},
},
});
const history: StatusHistory = [];
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
if (historyResult.success) {
history.push(...historyResult.data);
}
if (existingStatus) {
history.push({
label: existingStatus.label,
state: (existingStatus.state ?? undefined) as StatusUpdateState,
data: (existingStatus.data ?? undefined) as StatusUpdateData,
});
}
const updatedStatus = await tx.jobRunStatusRecord.upsert({
where: {
runId_key: {
runId,
key: id,
},
},
create: {
key: id,
runId,
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
label: status.label ?? id,
state: status.state,
data: status.data as any,
history: [],
},
update: {
label: status.label,
state: status.state,
data: status.data as any,
history: history as any[],
},
});
return updatedStatus;
});
return statusRecord;
}
}
@@ -1,3 +1,3 @@
import { action } from "./api.v1.tasks.$id.callback.$secret";
import { action } from "./api.v1.tasks.$id.callback.$secret/route";
export { action };
@@ -0,0 +1,92 @@
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
export class CompleteRunTaskService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
runId: string,
id: string,
taskBody: CompleteTaskBodyOutput
): Promise<ServerTask | undefined> {
const existingTask = await this.#prismaClient.task.findUnique({
where: {
id,
},
include: {
run: true,
attempts: {
where: {
status: "PENDING",
},
orderBy: {
number: "desc",
},
take: 1,
},
},
});
if (!existingTask) {
return;
}
if (existingTask.runId !== runId) {
return;
}
if (existingTask.run.environmentId !== environment.id) {
return;
}
if (
existingTask.status === "COMPLETED" ||
existingTask.status === "ERRORED" ||
existingTask.status === "CANCELED"
) {
logger.debug("Task already completed", {
existingTask,
});
return taskWithAttemptsToServerTask(existingTask);
}
if (existingTask.attempts.length === 1) {
await this.#prismaClient.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
},
});
}
const updatedTask = await this.#prismaClient.task.update({
where: {
id,
},
data: {
status: "COMPLETED",
output: taskBody.output as any,
outputIsUndefined: typeof taskBody.output === "undefined",
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
run: true,
},
});
return taskWithAttemptsToServerTask(updatedTask);
}
}
@@ -1,17 +1,16 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import type { CompleteTaskBodyOutput } from "@trigger.dev/core";
import {
API_VERSIONS,
CompleteTaskBodyInputSchema,
CompleteTaskBodyV2InputSchema,
} from "@trigger.dev/core";
import { z } from "zod";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { CompleteRunTaskService } from "./CompleteRunTaskService.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -118,90 +117,3 @@ async function completeRunTask(
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class CompleteRunTaskService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
runId: string,
id: string,
taskBody: CompleteTaskBodyOutput
): Promise<ServerTask | undefined> {
const existingTask = await this.#prismaClient.task.findUnique({
where: {
id,
},
include: {
run: true,
attempts: {
where: {
status: "PENDING",
},
orderBy: {
number: "desc",
},
take: 1,
},
},
});
if (!existingTask) {
return;
}
if (existingTask.runId !== runId) {
return;
}
if (existingTask.run.environmentId !== environment.id) {
return;
}
if (
existingTask.status === "COMPLETED" ||
existingTask.status === "ERRORED" ||
existingTask.status === "CANCELED"
) {
logger.debug("Task already completed", {
existingTask,
});
return taskWithAttemptsToServerTask(existingTask);
}
if (existingTask.attempts.length === 1) {
await this.#prismaClient.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
},
});
}
const updatedTask = await this.#prismaClient.task.update({
where: {
id,
},
data: {
status: "COMPLETED",
output: taskBody.output as any,
outputIsUndefined: typeof taskBody.output === "undefined",
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
run: true,
},
});
return taskWithAttemptsToServerTask(updatedTask);
}
}
@@ -1,76 +1,11 @@
import type { ActionFunctionArgs } 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 { PrismaClient, prisma } from "~/db.server";
import { FailTaskBodyInput, ServerTask } from "@trigger.dev/core";
import { PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { formatError } from "~/utils/formatErrors.server";
const ParamsSchema = z.object({
runId: z.string(),
id: z.string(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
const { runId, id } = ParamsSchema.parse(params);
// Now parse the request body
const anyBody = await request.json();
logger.debug("FailRunTaskService.call() request body", {
body: anyBody,
runId,
id,
});
const body = FailTaskBodyInputSchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new FailRunTaskService();
try {
const task = await service.call(authenticatedEnv, runId, id, body.data);
logger.debug("FailRunTaskService.call() response body", {
runId,
id,
task,
});
if (!task) {
return json({ message: "Task not found" }, { status: 404 });
}
return json(task);
} catch (error) {
if (error instanceof Error) {
return json({ error: error.message }, { status: 400 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class FailRunTaskService {
#prismaClient: PrismaClient;
@@ -0,0 +1,69 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { FailTaskBodyInputSchema } from "@trigger.dev/core";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { FailRunTaskService } from "./FailRunTaskService.server";
const ParamsSchema = z.object({
runId: z.string(),
id: z.string(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
const { runId, id } = ParamsSchema.parse(params);
// Now parse the request body
const anyBody = await request.json();
logger.debug("FailRunTaskService.call() request body", {
body: anyBody,
runId,
id,
});
const body = FailTaskBodyInputSchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new FailRunTaskService();
try {
const task = await service.call(authenticatedEnv, runId, id, body.data);
logger.debug("FailRunTaskService.call() response body", {
runId,
id,
task,
});
if (!task) {
return json({ message: "Task not found" }, { status: 404 });
}
return json(task);
} catch (error) {
if (error instanceof Error) {
return json({ error: error.message }, { status: 400 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
@@ -0,0 +1,49 @@
import { ServerTask, RunTaskResponseWithCachedTasksBody } from "@trigger.dev/core";
import { PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { prepareTasksForCaching } from "~/models/task.server";
export class ChangeRequestLazyLoadedCachedTasks {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
task: ServerTask,
cursor?: string | null
): Promise<RunTaskResponseWithCachedTasksBody> {
if (!cursor) {
return {
task,
};
}
// We need to limit the cached tasks to not be too large >2MB when serialized
const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000;
const nextTasks = await this.#prismaClient.task.findMany({
where: {
runId,
status: "COMPLETED",
noop: false,
},
take: 250,
cursor: {
id: cursor,
},
orderBy: {
id: "asc",
},
});
const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
return {
task,
cachedTasks: preparedTasks,
};
}
}
@@ -1,17 +1,11 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
API_VERSIONS,
RunTaskBodyOutputSchema,
RunTaskResponseWithCachedTasksBody,
ServerTask,
} from "@trigger.dev/core";
import { API_VERSIONS, RunTaskBodyOutputSchema } from "@trigger.dev/core";
import { z } from "zod";
import { PrismaClient, prisma } from "~/db.server";
import { prepareTasksForCaching } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { RunTaskService } from "~/services/tasks/runTask.server";
import { ChangeRequestLazyLoadedCachedTasks } from "./ChangeRequestLazyLoadedCachedTasks.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -111,48 +105,3 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: "Something went wrong" }, { status: 500 });
}
}
class ChangeRequestLazyLoadedCachedTasks {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
task: ServerTask,
cursor?: string | null
): Promise<RunTaskResponseWithCachedTasksBody> {
if (!cursor) {
return {
task,
};
}
// We need to limit the cached tasks to not be too large >2MB when serialized
const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000;
const nextTasks = await this.#prismaClient.task.findMany({
where: {
runId,
status: "COMPLETED",
noop: false,
},
take: 250,
cursor: {
id: cursor,
},
orderBy: {
id: "asc",
},
});
const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
return {
task,
cachedTasks: preparedTasks,
};
}
}
@@ -1,44 +1,8 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { ResumeTaskService } from "~/services/tasks/resumeTask.server";
import { workerQueue } from "~/services/worker.server";
const ParamsSchema = z.object({
id: z.string(),
secret: z.string(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
const { id } = ParamsSchema.parse(params);
// Parse body as JSON (no schema parsing)
const body = await request.json();
const service = new CallbackRunTaskService();
try {
// Complete task with request body as output
await service.call(id, body, request.url);
return json({ success: true });
} catch (error) {
if (error instanceof Error) {
logger.error("Error while processing task callback:", { error });
return json({ error: `Something went wrong: ${error.message}` }, { status: 500 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class CallbackRunTaskService {
#prismaClient: PrismaClient;
@@ -0,0 +1,38 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { logger } from "~/services/logger.server";
import { CallbackRunTaskService } from "./CallbackRunTaskService.server";
const ParamsSchema = z.object({
id: z.string(),
secret: z.string(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
const { id } = ParamsSchema.parse(params);
// Parse body as JSON (no schema parsing)
const body = await request.json();
const service = new CallbackRunTaskService();
try {
// Complete task with request body as output
await service.call(id, body, request.url);
return json({ success: true });
} catch (error) {
if (error instanceof Error) {
logger.error("Error while processing task callback:", { error });
return json({ error: `Something went wrong: ${error.message}` }, { status: 500 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
+27 -1
View File
@@ -1,10 +1,36 @@
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { redirect, type ActionFunction, type LoaderFunction } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
import {
clearCurrentProjectId,
commitCurrentProjectSession,
getCurrentProjectId,
} from "~/services/currentProject.server";
import { logoutPath } from "~/utils/pathBuilder";
export const action: ActionFunction = async ({ request }) => {
const projectId = await getCurrentProjectId(request);
if (projectId) {
const removeProjectIdSession = await clearCurrentProjectId(request);
return redirect(logoutPath(), {
headers: {
"Set-Cookie": await commitCurrentProjectSession(removeProjectIdSession),
},
});
}
return await authenticator.logout(request, { redirectTo: "/" });
};
export const loader: LoaderFunction = async ({ request }) => {
const projectId = await getCurrentProjectId(request);
if (projectId) {
const removeProjectIdSession = await clearCurrentProjectId(request);
return redirect(logoutPath(), {
headers: {
"Set-Cookie": await commitCurrentProjectSession(removeProjectIdSession),
},
});
}
return await authenticator.logout(request, { redirectTo: "/" });
};
@@ -1,76 +1,9 @@
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { TaskQueue } from "@trigger.dev/database";
import { Gauge, Registry } from "prom-client";
import { z } from "zod";
import { prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
import { marqs } from "~/v3/marqs/index.server";
const ParamsSchema = z.object({
projectRef: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}
const validatedParams = ParamsSchema.parse(params);
const user = await prisma.user.findUnique({
where: {
id: authenticationResult.userId,
},
});
if (!user) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}
const project = user.admin
? await prisma.project.findFirst({
where: {
externalRef: validatedParams.projectRef,
},
include: {
organization: true,
},
})
: await prisma.project.findFirst({
where: {
externalRef: validatedParams.projectRef,
organization: {
members: {
some: {
userId: authenticationResult.userId,
},
},
},
},
include: {
organization: true,
},
});
if (!project) {
return new Response("Not found", { status: 404 });
}
const registry = new Registry();
// Return prometheus metrics for the project (queues)
await registerProjectMetrics(registry, project.id, authenticationResult.userId);
return new Response(await registry.metrics(), {
headers: {
"Content-Type": registry.contentType,
},
});
}
export async function registerProjectMetrics(
registry: Registry,
projectId: string,
@@ -0,0 +1,51 @@
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { Registry } from "prom-client";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
import { registerProjectMetrics } from "./registerProjectMetrics.server";
const ParamsSchema = z.object({
projectRef: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}
const validatedParams = ParamsSchema.parse(params);
const project = await prisma.project.findFirst({
where: {
externalRef: validatedParams.projectRef,
organization: {
members: {
some: {
userId: authenticationResult.userId,
},
},
},
},
include: {
organization: true,
},
});
if (!project) {
return new Response("Not found", { status: 404 });
}
const registry = new Registry();
// Return prometheus metrics for the project (queues)
await registerProjectMetrics(registry, project.id, authenticationResult.userId);
return new Response(await registry.metrics(), {
headers: {
"Content-Type": registry.contentType,
},
});
}
@@ -34,7 +34,7 @@ import { TextLink } from "~/components/primitives/TextLink";
import { prisma } from "~/db.server";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { EditableScheduleElements } from "~/presenters/v3/EditSchedulePresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
@@ -92,9 +92,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
submission.value?.friendlyId === result.id ? "Schedule updated" : "Schedule created"
);
} catch (error: any) {
submission.error.taskIdentifier =
error instanceof Error ? error.message : JSON.stringify(error);
return json(submission, { status: 400 });
const errorMessage = `Failed: ${
error instanceof Error ? error.message : JSON.stringify(error)
}`;
return redirectWithErrorMessage(
v3SchedulesPath({ slug: organizationSlug }, { slug: projectParam }),
request,
errorMessage
);
}
};
@@ -4,11 +4,11 @@ import {
QueueListIcon,
StopCircleIcon,
} from "@heroicons/react/20/solid";
import { useFetcher, useParams } from "@remix-run/react";
import { useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3";
import { useEffect } from "react";
import { typedjson, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
import { typedjson, useTypedFetcher } from "remix-typedjson";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { CodeBlock } from "~/components/code/CodeBlock";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
@@ -46,6 +46,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
organizationSlug,
projectSlug: projectParam,
spanId: spanParam,
runFriendlyId: runParam,
});
if (!span) {
@@ -0,0 +1,210 @@
import { Form } from "@remix-run/react";
import { ActionFunctionArgs } from "@remix-run/server-runtime";
import { PlainClient } from "@team-plain/typescript-sdk";
import { z } from "zod";
import { MainCenteredContainer } from "~/components/layout/AppLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { useUser } from "~/hooks/useUser";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { logger } from "~/services/logger.server";
import { requireUser } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
import v3Icon from "~/assets/icons/v3.svg";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
const ParamSchema = z.object({
organizationSlug: z.string(),
});
export const action = async ({ request, params }: ActionFunctionArgs) => {
if (request.method.toLowerCase() !== "post") {
return redirectWithErrorMessage("/", request, "Invalid request method");
}
const user = await requireUser(request);
const { organizationSlug } = ParamSchema.parse(params);
const failedRedirectPath = organizationPath({ slug: organizationSlug });
try {
if (!env.PLAIN_API_KEY) {
return redirectWithErrorMessage(
failedRedirectPath,
request,
"Error requesting V3 access: Plain API key"
);
}
//mark them as having requested v3
const organization = await prisma.organization.update({
where: {
slug: organizationSlug,
members: {
some: {
userId: user.id,
},
},
},
data: {
hasRequestedV3: true,
},
});
//update Plain
const client = new PlainClient({
apiKey: env.PLAIN_API_KEY,
});
const upsertCustomerRes = await client.upsertCustomer({
identifier: {
emailAddress: user.email,
},
onCreate: {
fullName: user.name ?? user.email,
email: {
email: user.email,
isVerified: true,
},
},
onUpdate: {},
});
if (upsertCustomerRes.error) {
logger.error("Error upserting customer", upsertCustomerRes.error);
return redirectWithErrorMessage(failedRedirectPath, request, "Error requesting V3 access");
}
const groupResult = await client.addCustomerToCustomerGroups({
customerId: upsertCustomerRes.data.customer.id,
customerGroupIdentifiers: [
{
customerGroupKey: "interested-in-v3",
},
],
});
if (groupResult.error) {
logger.error("Error adding customer to group", groupResult.error);
return redirectWithErrorMessage(failedRedirectPath, request, "Error requesting V3 access");
}
const createThreadRes = await client.createThread({
customerIdentifier: {
customerId: upsertCustomerRes.data.customer.id,
},
title: "v3 early access request",
components: [
{
componentText: {
text: `${upsertCustomerRes.data.customer.email.email} has been added to the v3 early access group`,
},
},
{
componentText: {
text: `Company: ${organization.title ?? ""}`,
},
},
],
});
if (createThreadRes.error) {
logger.error("Error creating thread", createThreadRes.error);
return redirectWithErrorMessage(failedRedirectPath, request, "Error requesting V3 access");
}
return redirectWithSuccessMessage(
organizationPath(organization),
request,
"V3 access requested"
);
} catch (error) {
logger.error("Error requesting V3 access", { error });
return redirectWithErrorMessage(failedRedirectPath, request, "Error requesting V3 access");
}
};
export function RequestV3Access({
hasRequestedV3,
organizationSlug,
projectsCount,
}: {
hasRequestedV3: boolean;
organizationSlug: string;
projectsCount: number;
}) {
const user = useUser();
if (hasRequestedV3) {
return (
<MainCenteredContainer>
<div>
<div className="relative mb-4 flex size-9">
<img src={v3Icon} alt="v3" width={32} height={32} />
<div className="absolute right-0 top-0 size-4 rounded-full bg-background-dimmed">
<CheckCircleIcon className="size-4 text-success" />
</div>
</div>
<Paragraph spacing variant="base/bright">
Weve received your request for v3 and well notify you as soon as you have access.
Were granting new users access every day so you wont be waiting long.
</Paragraph>
<Paragraph spacing variant="base/bright">
Right now v3 is completely free to use but{" "}
<TextLink href="https://trigger.dev/blog/v3-developer-preview-launch/#cloud-pricing">
paid tiers
</TextLink>{" "}
will be introduced soon.
</Paragraph>
<Paragraph spacing variant="base/bright">
In the meantime, check out the{" "}
<TextLink href="https://trigger.dev/docs">v3 docs</TextLink>, the{" "}
<TextLink href="https://trigger.dev/blog/v3-developer-preview-launch/">
v3 blog post
</TextLink>{" "}
and <TextLink href="https://trigger.dev/discord">join our Discord</TextLink>.
</Paragraph>
</div>
</MainCenteredContainer>
);
}
return (
<MainCenteredContainer>
<img src={v3Icon} alt="v3" width={32} height={32} className="mb-4" />
<Form action={`/resources/orgs/${organizationSlug}/v3-access`} method="post">
{projectsCount > 0 ? (
<Paragraph spacing variant="base/bright">
You can no longer create v2 projects and your organization doesn't have access to v3
yet. We are approving access requests daily.
</Paragraph>
) : (
<Paragraph spacing variant="base/bright">
Trigger.dev v3 is currently in Developer Preview and were operating a waitlist as we
focus on the platforms reliability and scaleability.
</Paragraph>
)}
<Paragraph spacing variant="base/bright">
For more info, check out our{" "}
<TextLink href="https://trigger.dev/blog/v3-developer-preview-launch/">
v3 blog post
</TextLink>
.
</Paragraph>
<div className="mt-2 flex items-center justify-between gap-3">
{projectsCount > 0 ? (
<LinkButton variant="tertiary/small" to={organizationPath({ slug: organizationSlug })}>
Cancel
</LinkButton>
) : null}
<Button variant="primary/small" type="submit">
Request access
</Button>
</div>
</Form>
</MainCenteredContainer>
);
}
+117
View File
@@ -4,6 +4,14 @@ import {
findEnvironmentByApiKey,
findEnvironmentByPublicApiKey,
} from "~/models/runtimeEnvironment.server";
import {
PersonalAccessTokenAuthenticationResult,
authenticateApiRequestWithPersonalAccessToken,
isPersonalAccessToken,
} from "./personalAccessToken.server";
import { prisma } from "~/db.server";
import { json } from "@remix-run/server-runtime";
import { findProjectByRef } from "~/models/project.server";
type Optional<T, K extends keyof T> = Prettify<Omit<T, K> & Partial<Pick<T, K>>>;
@@ -92,3 +100,112 @@ export function getApiKeyResult(apiKey: string) {
const type = isPublicApiKey(apiKey) ? ("PUBLIC" as const) : ("PRIVATE" as const);
return { apiKey, type };
}
export type DualAuthenticationResult =
| {
type: "personalAccessToken";
result: PersonalAccessTokenAuthenticationResult;
}
| {
type: "apiKey";
result: ApiAuthenticationResult;
};
export async function authenticateProjectApiKeyOrPersonalAccessToken(
request: Request
): Promise<DualAuthenticationResult | undefined> {
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
return;
}
if (isPersonalAccessToken(apiKey)) {
const result = await authenticateApiRequestWithPersonalAccessToken(request);
if (!result) {
return;
}
return {
type: "personalAccessToken",
result,
};
}
const result = await authenticateApiKey(apiKey, { allowPublicKey: false });
if (!result) {
return;
}
return {
type: "apiKey",
result,
};
}
export async function authenticatedEnvironmentForAuthentication(
auth: DualAuthenticationResult,
projectRef: string,
slug: string
): Promise<AuthenticatedEnvironment> {
switch (auth.type) {
case "apiKey": {
if (auth.result.environment.project.externalRef !== projectRef) {
throw json(
{
error:
"Invalid project ref for this API key. Make sure you are using an API key associated with that project.",
},
{ status: 400 }
);
}
if (auth.result.environment.slug !== slug) {
throw json(
{
error:
"Invalid environment slug for this API key. Make sure you are using an API key associated with that environment.",
},
{ status: 400 }
);
}
return auth.result.environment;
}
case "personalAccessToken": {
const user = await prisma.user.findUnique({
where: {
id: auth.result.userId,
},
});
if (!user) {
throw json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const project = await findProjectByRef(projectRef, user.id);
if (!project) {
throw json({ error: "Project not found" }, { status: 404 });
}
const environment = await prisma.runtimeEnvironment.findFirst({
where: {
projectId: project.id,
slug: slug,
},
include: {
project: true,
organization: true,
},
});
if (!environment) {
throw json({ error: "Environment not found" }, { status: 404 });
}
return environment;
}
}
}
@@ -157,16 +157,18 @@ export function authorizationRateLimitMiddleware({
}
res.setHeader("Content-Type", "application/problem+json");
const secondsUntilReset = Math.max(0, (reset - new Date().getTime()) / 1000);
return res.status(429).send(
JSON.stringify(
{
title: "Rate Limit Exceeded",
status: 429,
type: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429",
detail: `Rate limit exceeded ${remaining}/${limit} requests remaining. Retry after ${reset} seconds.`,
reset: reset,
limit: limit,
error: `Rate limit exceeded ${remaining}/${limit} requests remaining. Retry after ${reset} seconds.`,
detail: `Rate limit exceeded ${remaining}/${limit} requests remaining. Retry in ${secondsUntilReset} seconds.`,
reset,
limit,
secondsUntilReset,
error: `Rate limit exceeded ${remaining}/${limit} requests remaining. Retry in ${secondsUntilReset} seconds.`,
},
null,
2
@@ -4,7 +4,7 @@ import { AuthenticatedEnvironment } from "../apiAuth.server";
import { EndpointApi } from "../endpointApi.server";
import { workerQueue } from "../worker.server";
import { env } from "~/env.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { RuntimeEnvironmentType } from "~/database-types";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
@@ -5,7 +5,7 @@ import { AuthenticatedEnvironment } from "../apiAuth.server";
import { workerQueue } from "../worker.server";
import { CreateEndpointError } from "./createEndpoint.server";
import { EndpointApi } from "../endpointApi.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { RuntimeEnvironmentType } from "~/database-types";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
@@ -1,11 +1,14 @@
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { JobRunStatus } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { $transaction, prisma } from "~/db.server";
import type { AuthenticatedEnvironment } from "../apiAuth.server";
import { CancelRunService } from "../runs/cancelRun.server";
import { logger } from "../logger.server";
import { CancelRunsForEvent } from "@trigger.dev/core";
import type { CancelRunsForEvent } from "@trigger.dev/core";
import type { JobRunStatus as JobRunStatusType } from "@trigger.dev/database";
import { JobRunStatus } from "~/database-types";
const CANCELLABLE_JOB_RUN_STATUS: JobRunStatus[] = [
const CANCELLABLE_JOB_RUN_STATUS: Array<JobRunStatusType> = [
JobRunStatus.PENDING,
JobRunStatus.QUEUED,
JobRunStatus.WAITING_ON_CONNECTIONS,
@@ -1,11 +1,11 @@
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { JobRunStatus } from "@trigger.dev/database";
import { CancelRunService } from "../runs/cancelRun.server";
import { logger } from "../logger.server";
import { CancelRunsForJob } from "@trigger.dev/core";
import { JobRunStatus } from "~/database-types";
const CANCELLABLE_JOB_RUN_STATUS: JobRunStatus[] = [
const CANCELLABLE_JOB_RUN_STATUS: Array<keyof typeof JobRunStatus> = [
JobRunStatus.PENDING,
JobRunStatus.QUEUED,
JobRunStatus.WAITING_ON_CONNECTIONS,
@@ -90,7 +90,7 @@ export async function revokePersonalAccessToken(tokenId: string) {
});
}
type PersonalAccessTokenAuthenticationResult = {
export type PersonalAccessTokenAuthenticationResult = {
userId: string;
};
@@ -169,6 +169,10 @@ export async function authenticatePersonalAccessToken(
};
}
export function isPersonalAccessToken(token: string) {
return token.startsWith(tokenPrefix);
}
export function createAuthorizationCode() {
return prisma.authorizationCode.create({
data: {
@@ -34,7 +34,7 @@ import { detectResponseIsTimeout } from "~/models/endpoint.server";
import { isRunCompleted } from "~/models/jobRun.server";
import { resolveRunConnections } from "~/models/runConnection.server";
import { prepareTasksForCaching, prepareTasksForCachingLegacy } from "~/models/task.server";
import { CompleteRunTaskService } from "~/routes/api.v1.runs.$runId.tasks.$id.complete";
import { CompleteRunTaskService } from "~/routes/api.v1.runs.$runId.tasks.$id.complete/CompleteRunTaskService.server";
import { formatError } from "~/utils/formatErrors.server";
import { safeJsonZodParse } from "~/utils/json";
import { EndpointApi } from "../endpointApi.server";
@@ -441,6 +441,10 @@ export class PerformRunExecutionV3Service {
await this.#resumeAutoYieldedRunWithCompletedTask(run, safeBody.data, durationInMs);
break;
}
case "AUTO_YIELD_RATE_LIMIT": {
await this.#rescheduleRun(run, safeBody.data.reset, durationInMs);
break;
}
case "RESUME_WITH_PARALLEL_TASK": {
await this.#resumeParallelRunWithTask(run, safeBody.data, durationInMs);
@@ -667,6 +671,10 @@ export class PerformRunExecutionV3Service {
break;
}
case "AUTO_YIELD_RATE_LIMIT": {
await this.#rescheduleRun(run, childError.reset, durationInMs);
break;
}
case "CANCELED": {
break;
}
@@ -801,9 +809,9 @@ export class PerformRunExecutionV3Service {
});
}
async #resumeAutoYieldedRun(
async #rescheduleRun(
run: FoundRun,
data: AutoYieldMetadata,
reset: number,
durationInMs: number,
executionCount: number = 1
) {
@@ -820,16 +828,6 @@ export class PerformRunExecutionV3Service {
executionCount: {
increment: executionCount,
},
autoYieldExecution: {
create: [
{
location: data.location,
timeRemaining: data.timeRemaining,
timeElapsed: data.timeElapsed,
limit: data.limit ?? 0,
},
],
},
forceYieldImmediately: false,
},
select: {
@@ -837,7 +835,7 @@ export class PerformRunExecutionV3Service {
},
});
await ResumeRunService.enqueue(run, tx);
await ResumeRunService.enqueue(run, tx, new Date(reset));
});
}
@@ -888,6 +886,46 @@ export class PerformRunExecutionV3Service {
});
}
async #resumeAutoYieldedRun(
run: FoundRun,
data: AutoYieldMetadata,
durationInMs: number,
executionCount: number = 1
) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.jobRun.update({
where: {
id: run.id,
},
data: {
status: "WAITING_TO_EXECUTE",
executionDuration: {
increment: durationInMs,
},
executionCount: {
increment: executionCount,
},
autoYieldExecution: {
create: [
{
location: data.location,
timeRemaining: data.timeRemaining,
timeElapsed: data.timeElapsed,
limit: data.limit ?? 0,
},
],
},
forceYieldImmediately: false,
},
select: {
executionCount: true,
},
});
await ResumeRunService.enqueue(run, tx);
});
}
async #retryRunWithTask(
run: FoundRun,
data: RunJobRetryWithTask,
@@ -4,9 +4,7 @@ import { env } from "~/env.server";
import nodeCrypto from "node:crypto";
import { safeJsonParse } from "~/utils/json";
import { logger } from "../logger.server";
export const SecretStoreOptionsSchema = z.enum(["DATABASE", "AWS_PARAM_STORE"]);
export type SecretStoreOptions = z.infer<typeof SecretStoreOptionsSchema>;
import type { SecretStoreOptions } from "./secretStoreOptionsSchema.server";
type ProviderInitializationOptions = {
DATABASE: {
@@ -0,0 +1,4 @@
import { z } from "zod";
export const SecretStoreOptionsSchema = z.enum(["DATABASE", "AWS_PARAM_STORE"]);
export type SecretStoreOptions = z.infer<typeof SecretStoreOptionsSchema>;
@@ -1,10 +1,8 @@
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { createHttpSourceRequest } from "~/utils/createHttpSourceRequest";
import { logger } from "../logger.server";
import { RuntimeEnvironmentType } from "~/database-types";
export class HandleHttpSourceService {
#prismaClient: PrismaClient;
@@ -1,10 +1,10 @@
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { createHttpSourceRequest } from "~/utils/createHttpSourceRequest";
import { WebhookContextMetadata } from "@trigger.dev/core";
import { createHash } from "crypto";
import { RuntimeEnvironmentType } from "~/database-types";
export class HandleWebhookRequestService {
#prismaClient: PrismaClient;
@@ -7,6 +7,8 @@ import { getSecretStore } from "~/services/secrets/secretStore.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import {
CreateResult,
DeleteEnvironmentVariable,
DeleteEnvironmentVariableValue,
EnvironmentVariable,
ProjectEnvironmentVariable,
Repository,
@@ -41,9 +43,8 @@ export class EnvironmentVariablesRepository implements Repository {
async create(
projectId: string,
userId: string,
options: {
overwrite: boolean;
override: boolean;
environmentIds: string[];
variables: {
key: string;
@@ -54,13 +55,6 @@ export class EnvironmentVariablesRepository implements Repository {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
@@ -92,6 +86,15 @@ export class EnvironmentVariablesRepository implements Repository {
return { success: false as const, error: `Environment not found` };
}
// Check to see if any of the variables are `TRIGGER_SECRET_KEY` or `TRIGGER_API_URL`
const triggerKeys = options.variables.map((v) => v.key);
if (triggerKeys.includes("TRIGGER_SECRET_KEY") || triggerKeys.includes("TRIGGER_API_URL")) {
return {
success: false as const,
error: `You cannot set the variables TRIGGER_SECRET_KEY or TRIGGER_API_URL as they will be set automatically`,
};
}
//get rid of empty variables
const values = options.variables.filter((v) => v.key.trim() !== "" && v.value.trim() !== "");
if (values.length === 0) {
@@ -99,7 +102,7 @@ export class EnvironmentVariablesRepository implements Repository {
}
//check if any of them exist in an environment we're setting
if (!options.overwrite) {
if (!options.override) {
const existingVariableKeys: { key: string; environments: RuntimeEnvironmentType[] }[] = [];
for (const variable of values) {
const existingVariable = project.environmentVariables.find((v) => v.key === variable.key);
@@ -119,7 +122,7 @@ export class EnvironmentVariablesRepository implements Repository {
if (existingVariableKeys.length > 0) {
return {
success: false as const,
error: `Some of the variables are already set for these environments`,
error: `Some of the variables are already set for these environments. Set override to true to override them.`,
variableErrors: existingVariableKeys.map((val) => ({
key: val.key,
error: `Variable already set in ${val.environments
@@ -217,19 +220,15 @@ export class EnvironmentVariablesRepository implements Repository {
async edit(
projectId: string,
userId: string,
options: { values: { value: string; environmentId: string }[]; id: string }
options: {
values: { value: string; environmentId: string }[];
id: string;
keepEmptyValues?: boolean;
}
): Promise<Result> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
@@ -237,18 +236,6 @@ export class EnvironmentVariablesRepository implements Repository {
select: {
id: true,
},
where: {
OR: [
{
orgMember: null,
},
{
orgMember: {
userId,
},
},
],
},
},
},
});
@@ -266,12 +253,15 @@ export class EnvironmentVariablesRepository implements Repository {
//add in empty values for environments that don't have a value
const environmentIds = project.environments.map((e) => e.id);
for (const environmentId of environmentIds) {
if (!values.some((v) => v.environmentId === environmentId)) {
values.push({
environmentId,
value: "",
});
if (!options.keepEmptyValues) {
for (const environmentId of environmentIds) {
if (!values.some((v) => v.environmentId === environmentId)) {
values.push({
environmentId,
value: "",
});
}
}
}
@@ -364,17 +354,10 @@ export class EnvironmentVariablesRepository implements Repository {
}
}
async getProject(projectId: string, userId: string): Promise<ProjectEnvironmentVariable[]> {
async getProject(projectId: string): Promise<ProjectEnvironmentVariable[]> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
@@ -446,19 +429,12 @@ export class EnvironmentVariablesRepository implements Repository {
async getEnvironment(
projectId: string,
userId: string,
environmentId: string
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
@@ -477,7 +453,7 @@ export class EnvironmentVariablesRepository implements Repository {
return [];
}
return this.getEnvironmentVariables(projectId, environmentId);
return this.getEnvironmentVariables(projectId, environmentId, excludeInternalVariables);
}
async #getTriggerEnvironmentVariables(environmentId: string): Promise<EnvironmentVariable[]> {
@@ -621,25 +597,24 @@ export class EnvironmentVariablesRepository implements Repository {
async getEnvironmentVariables(
projectId: string,
environmentId: string
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]> {
const secretEnvVars = await this.#getSecretEnvironmentVariables(projectId, environmentId);
if (excludeInternalVariables) {
return secretEnvVars;
}
const triggerEnvVars = await this.#getTriggerEnvironmentVariables(environmentId);
return [...secretEnvVars, ...triggerEnvVars];
}
async delete(projectId: string, userId: string, options: { id: string }): Promise<Result> {
async delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
organization: {
members: {
some: {
userId,
},
},
},
deletedAt: null,
},
select: {
@@ -647,18 +622,6 @@ export class EnvironmentVariablesRepository implements Repository {
select: {
id: true,
},
where: {
OR: [
{
orgMember: null,
},
{
orgMember: {
userId,
},
},
],
},
},
},
});
@@ -703,7 +666,7 @@ export class EnvironmentVariablesRepository implements Repository {
prismaClient: tx,
});
//create the secret values and references
//delete the secret values and references
for (const value of environmentVariable.values) {
const key = secretKey(projectId, value.environmentId, environmentVariable.key);
await secretStore.deleteSecret(key);
@@ -728,4 +691,94 @@ export class EnvironmentVariablesRepository implements Repository {
};
}
}
async deleteValue(projectId: string, options: DeleteEnvironmentVariableValue): Promise<Result> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
const environmentVariable = await this.prismaClient.environmentVariable.findUnique({
select: {
id: true,
key: true,
values: {
select: {
id: true,
environmentId: true,
valueReference: {
select: {
key: true,
},
},
},
},
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
const value = environmentVariable.values.find((v) => v.environmentId === options.environmentId);
if (!value) {
return { success: false as const, error: "Environment variable value not found" };
}
// If this is the last value, delete the whole variable
if (environmentVariable.values.length === 1) {
return this.delete(projectId, { id: options.id });
}
try {
await $transaction(this.prismaClient, async (tx) => {
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
const key = secretKey(projectId, options.environmentId, environmentVariable.key);
await secretStore.deleteSecret(key);
if (value.valueReference) {
await tx.secretReference.delete({
where: {
key: value.valueReference.key,
},
});
}
await tx.environmentVariableValue.delete({
where: {
id: value.id,
},
});
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
}
@@ -31,14 +31,22 @@ export const EditEnvironmentVariable = z.object({
value: z.string(),
})
),
keepEmptyValues: z.boolean().optional(),
});
export type EditEnvironmentVariable = z.infer<typeof EditEnvironmentVariable>;
export const DeleteEnvironmentVariable = z.object({
id: z.string(),
environmentId: z.string().optional(),
});
export type DeleteEnvironmentVariable = z.infer<typeof DeleteEnvironmentVariable>;
export const DeleteEnvironmentVariableValue = z.object({
id: z.string(),
environmentId: z.string(),
});
export type DeleteEnvironmentVariableValue = z.infer<typeof DeleteEnvironmentVariableValue>;
export type Result =
| {
success: true;
@@ -65,18 +73,19 @@ export type EnvironmentVariable = {
};
export interface Repository {
create(
projectId: string,
userId: string,
options: CreateEnvironmentVariables
): Promise<CreateResult>;
edit(projectId: string, userId: string, options: EditEnvironmentVariable): Promise<Result>;
getProject(projectId: string, userId: string): Promise<ProjectEnvironmentVariable[]>;
create(projectId: string, options: CreateEnvironmentVariables): Promise<CreateResult>;
edit(projectId: string, options: EditEnvironmentVariable): Promise<Result>;
getProject(projectId: string): Promise<ProjectEnvironmentVariable[]>;
getEnvironment(
projectId: string,
userId: string,
environmentId: string
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]>;
getEnvironmentVariables(projectId: string, environmentId: string): Promise<EnvironmentVariable[]>;
delete(projectId: string, userId: string, options: DeleteEnvironmentVariable): Promise<Result>;
getEnvironmentVariables(
projectId: string,
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]>;
delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result>;
deleteValue(projectId: string, options: DeleteEnvironmentVariableValue): Promise<Result>;
}
+51 -24
View File
@@ -23,7 +23,7 @@ import { Prisma, TaskEvent, TaskEventStatus, type TaskEventKind } from "@trigger
import Redis, { RedisOptions } from "ioredis";
import { createHash } from "node:crypto";
import { EventEmitter } from "node:stream";
import { PrismaClient, prisma } from "~/db.server";
import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
@@ -102,9 +102,27 @@ export type QueryOptions = Prisma.TaskEventWhereInput;
export type TaskEventRecord = TaskEvent;
export type QueriedEvent = TaskEvent;
export type QueriedEvent = Prisma.TaskEventGetPayload<{
select: {
id: true;
spanId: true;
parentId: true;
runId: true;
idempotencyKey: true;
message: true;
style: true;
startTime: true;
duration: true;
isError: true;
isPartial: true;
isCancelled: true;
level: true;
events: true;
environmentType: true;
};
}>;
export type PreparedEvent = Omit<TaskEventRecord, "events" | "style" | "duration"> & {
export type PreparedEvent = Omit<QueriedEvent, "events" | "style" | "duration"> & {
duration: number;
events: SpanEvents;
style: TaskEventStyle;
@@ -140,6 +158,7 @@ export type SpanSummary = {
isPartial: boolean;
isCancelled: boolean;
level: NonNullable<CreatableEvent["level"]>;
environmentType: CreatableEventEnvironmentType;
};
};
@@ -162,7 +181,11 @@ export class EventRepository {
return this._subscriberCount;
}
constructor(private db: PrismaClient = prisma, private readonly _config: EventRepoConfig) {
constructor(
private db: PrismaClient = prisma,
private readReplica: PrismaReplicaClient = $replica,
private readonly _config: EventRepoConfig
) {
this._flushScheduler = new DynamicFlushScheduler({
batchSize: _config.batchSize,
flushInterval: _config.batchInterval,
@@ -351,7 +374,24 @@ export class EventRepository {
}
public async getTraceSummary(traceId: string): Promise<TraceSummary | undefined> {
const events = await this.db.taskEvent.findMany({
const events = await this.readReplica.taskEvent.findMany({
select: {
id: true,
spanId: true,
parentId: true,
runId: true,
idempotencyKey: true,
message: true,
style: true,
startTime: true,
duration: true,
isError: true,
isPartial: true,
isCancelled: true,
level: true,
events: true,
environmentType: true,
},
where: {
traceId,
},
@@ -386,6 +426,7 @@ export class EventRepository {
startTime: getDateFromNanoseconds(event.startTime),
level: event.level,
events: event.events,
environmentType: event.environmentType,
},
};
});
@@ -409,22 +450,8 @@ export class EventRepository {
// A Span can be cancelled if it is partial and has a parent that is cancelled
// And a span's duration, if it is partial and has a cancelled parent, is the time between the start of the span and the time of the cancellation event of the parent
public async getSpan(spanId: string) {
const traceSearch = await this.db.taskEvent.findFirst({
where: {
spanId,
},
select: {
traceId: true,
environmentType: true,
},
});
if (!traceSearch) {
return;
}
const traceSummary = await this.getTraceSummary(traceSearch.traceId);
public async getSpan(spanId: string, traceId: string) {
const traceSummary = await this.getTraceSummary(traceId);
const span = traceSummary?.spans.find((span) => span.id === spanId);
@@ -432,7 +459,7 @@ export class EventRepository {
return;
}
const fullEvent = await this.db.taskEvent.findUnique({
const fullEvent = await this.readReplica.taskEvent.findUnique({
where: {
id: span.recordId,
},
@@ -487,7 +514,7 @@ export class EventRepository {
const events = transformEvents(
span.data.events,
fullEvent.metadata as Attributes,
traceSearch.environmentType === "DEVELOPMENT"
traceSummary?.rootSpan.data.environmentType === "DEVELOPMENT"
);
return {
@@ -821,7 +848,7 @@ export class EventRepository {
export const eventRepository = singleton("eventRepo", initializeEventRepo);
function initializeEventRepo() {
const repo = new EventRepository(prisma, {
const repo = new EventRepository(prisma, $replica, {
batchSize: env.EVENTS_BATCH_SIZE,
batchInterval: env.EVENTS_BATCH_INTERVAL,
retentionInDays: env.EVENTS_DEFAULT_LOG_RETENTION,
@@ -99,10 +99,15 @@ export class TriggerTaskService extends BaseService {
})
: undefined;
const counter = await tx.taskRunCounter.upsert({
where: { taskIdentifier: taskId },
const counter = await tx.taskRunNumberCounter.upsert({
where: {
taskIdentifier_environmentId: {
taskIdentifier: taskId,
environmentId: environment.id,
},
},
update: { lastNumber: { increment: 1 } },
create: { taskIdentifier: taskId, lastNumber: 1 },
create: { taskIdentifier: taskId, environmentId: environment.id, lastNumber: 1 },
select: { lastNumber: true },
});
+3 -2
View File
@@ -9,6 +9,7 @@
"build:remix": "remix build",
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build",
"dev": "cross-env PORT=3030 remix dev -c \"node ./build/server.js\"",
"dev:worker": "cross-env NODE_PATH=../../node_modules/.pnpm/node_modules node ./build/server.js",
"format": "prettier --write .",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
"start": "cross-env NODE_ENV=production node --max-old-space-size=8192 ./build/server.js",
@@ -136,7 +137,7 @@
"parse-duration": "^1.1.0",
"posthog-js": "^1.93.3",
"posthog-node": "^3.1.3",
"prism-react-renderer": "^1.3.5",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
"prom-client": "^15.1.0",
"random-words": "^2.0.0",
@@ -237,4 +238,4 @@
"engines": {
"node": ">=16.0.0"
}
}
}
+2
View File
@@ -20,6 +20,8 @@ module.exports = {
"highlight.run",
"random-words",
"superjson",
"prismjs/components/prism-json",
"prismjs/components/prism-typescript",
],
browserNodeBuiltinsPolyfill: { modules: { path: true, os: true, crypto: true } },
watchPaths: async () => {
+93 -20
View File
@@ -1,8 +1,20 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
"openapi": ["/openapi.yml", "/v3-openapi.json"],
"versions": ["v3 (Developer Preview)", "v2"],
"openapi": [
"/openapi.yml",
"/v3-openapi.yaml"
],
"versions": [
"v3 (Developer Preview)",
"v2"
],
"api": {
"playground": {
"mode": "hide"
},
"maintainOrder": true
},
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png",
@@ -90,12 +102,19 @@
{
"group": "",
"version": "v3 (Developer Preview)",
"pages": ["v3/introduction"]
"pages": [
"v3/introduction"
]
},
{
"group": "Getting Started",
"version": "v3 (Developer Preview)",
"pages": ["v3/quick-start", "v3/upgrading-from-v2", "v3/changelog", "v3/feature-matrix"]
"pages": [
"v3/quick-start",
"v3/upgrading-from-v2",
"v3/changelog",
"v3/feature-matrix"
]
},
{
"group": "Fundamentals",
@@ -107,7 +126,10 @@
"v3/apikeys",
{
"group": "Task types",
"pages": ["v3/tasks-regular", "v3/tasks-scheduled"]
"pages": [
"v3/tasks-regular",
"v3/tasks-scheduled"
]
},
"v3/trigger-config"
]
@@ -115,7 +137,10 @@
{
"group": "Development",
"version": "v3 (Developer Preview)",
"pages": ["v3/cli-dev", "v3/run-tests"]
"pages": [
"v3/cli-dev",
"v3/run-tests"
]
},
{
"group": "Deployment",
@@ -126,7 +151,9 @@
"v3/github-actions",
{
"group": "Deployment integrations",
"pages": ["v3/vercel-integration"]
"pages": [
"v3/vercel-integration"
]
}
]
},
@@ -178,13 +205,28 @@
"v3/management-deactivate-schedule",
"v3/management-activate-schedule"
]
},
{
"group": "Env Vars API",
"pages": [
"v3/management-envvars-list",
"v3/management-envvars-import",
"v3/management-envvars-create",
"v3/management-envvars-retrieve",
"v3/management-envvars-update",
"v3/management-envvars-delete"
]
}
]
},
{
"group": "Open source",
"version": "v3 (Developer Preview)",
"pages": ["v3/github-repo", "v3/open-source-self-hosting", "v3/open-source-contributing"]
"pages": [
"v3/github-repo",
"v3/open-source-self-hosting",
"v3/open-source-contributing"
]
},
{
"group": "Troubleshooting",
@@ -199,7 +241,11 @@
{
"group": "Help",
"version": "v3 (Developer Preview)",
"pages": ["v3/community", "v3/help-slack", "v3/help-email"]
"pages": [
"v3/community",
"v3/help-slack",
"v3/help-email"
]
},
{
"group": "Getting Started",
@@ -391,7 +437,10 @@
"pages": [
{
"group": "Airtable",
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
"pages": [
"integrations/apis/airtable",
"integrations/apis/airtable-tasks"
]
},
{
"group": "GitHub",
@@ -417,16 +466,25 @@
},
{
"group": "Plain",
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
"pages": [
"integrations/apis/plain",
"integrations/apis/plain-tasks"
]
},
"integrations/apis/replicate",
{
"group": "SendGrid",
"pages": ["integrations/apis/sendgrid", "integrations/apis/sendgrid-tasks"]
"pages": [
"integrations/apis/sendgrid",
"integrations/apis/sendgrid-tasks"
]
},
{
"group": "Resend",
"pages": ["integrations/apis/resend", "integrations/apis/resend-tasks"]
"pages": [
"integrations/apis/resend",
"integrations/apis/resend-tasks"
]
},
{
"group": "Shopify",
@@ -438,7 +496,10 @@
},
{
"group": "Slack",
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
"pages": [
"integrations/apis/slack",
"integrations/apis/slack-tasks"
]
},
"integrations/apis/stripe",
{
@@ -464,7 +525,9 @@
"sdk/triggerclient/constructor",
{
"group": "Instance properties",
"pages": ["sdk/triggerclient/store"]
"pages": [
"sdk/triggerclient/store"
]
},
{
"group": "Instance methods",
@@ -527,7 +590,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -538,7 +604,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -551,7 +620,9 @@
{
"group": "HTTP Reference",
"version": "v2",
"pages": ["sdk/api-reference/events/create-an-event"]
"pages": [
"sdk/api-reference/events/create-an-event"
]
},
{
"group": "React SDK",
@@ -567,7 +638,9 @@
{
"group": "Overview",
"version": "v2",
"pages": ["examples/introduction"]
"pages": [
"examples/introduction"
]
}
],
"footerSocials": {
@@ -575,4 +648,4 @@
"github": "https://github.com/triggerdotdev",
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
}
}
}
-942
View File
@@ -1,942 +0,0 @@
{
"openapi": "3.1.0",
"info": {
"title": "Trigger.dev v3 REST API",
"description": "The REST API lets you trigger and manage runs on Trigger.dev. You can trigger a run, get the status of a run, and get the results of a run. ",
"version": "2024-04"
},
"servers": [
{
"url": "https://api.trigger.dev",
"description": "Trigger.dev API"
}
],
"paths": {
"/api/v1/schedules": {
"post": {
"operationId": "create_schedule_v1",
"description": "Create a new schedule based on the specified options.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleOptions"
}
}
}
},
"responses": {
"200": {
"description": "Schedule created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleObject"
}
}
}
},
"400": {
"description": "Invalid request parameters"
},
"422": {
"description": "Unprocessable Entity"
},
"401": {
"description": "Unauthorized"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nconst schedule = await schedules.create({\n task: 'my-task',\n cron: '0 0 * * *'\n});"
},
{
"lang": "sh",
"source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/schedules \\\n\t--header 'Authorization: Bearer <token>' \\\n\t--header 'Content-Type: application/json' \\\n\t--data '{\"task\":\"my-task\",\"cron\":\"0 0 * * *\"}'"
}
]
},
"get": {
"operationId": "list_schedules_v1",
"description": "List all schedules.",
"parameters": [
{
"in": "query",
"name": "page",
"schema": {
"type": "integer"
},
"required": false,
"description": "Page number of the schedule listing"
},
{
"in": "query",
"name": "perPage",
"schema": {
"type": "integer"
},
"required": false,
"description": "Number of schedules per page"
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListSchedulesResult"
}
}
}
},
"401": {
"description": "Unauthorized request"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nconst allSchedules = await schedules.list();"
},
{
"lang": "sh",
"source": "curl --request GET \\\n\t--url https://api.trigger.dev/api/v1/schedules \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
},
"/api/v1/schedules/{schedule_id}": {
"get": {
"operationId": "get_schedule_v1",
"description": "Get a schedule by its ID.",
"parameters": [
{
"in": "path",
"name": "schedule_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the schedule."
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleObject"
}
}
}
},
"401": {
"description": "Unauthorized request"
},
"404": {
"description": "Resource not found"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nconst schedule = await schedules.retrieve(scheduleId);"
},
{
"lang": "sh",
"source": "curl --request GET \\\n\t--url https://api.trigger.dev/api/v1/schedules/{schedule_id} \\\n\t--header 'Authorization: Bearer <token>'"
}
]
},
"put": {
"operationId": "update_schedule_v1",
"description": "Update a schedule by its ID.",
"parameters": [
{
"in": "path",
"name": "schedule_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the schedule."
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleOptions"
}
}
}
},
"responses": {
"200": {
"description": "Schedule updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleObject"
}
}
}
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Resource not found"
},
"422": {
"description": "Unprocessable Entity"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nconst updatedSchedule = await schedules.update(scheduleId, {\n task: 'my-updated-task',\n cron: '0 0 * * *'\n});"
},
{
"lang": "sh",
"source": "curl --request PUT \\\n\t--url https://api.trigger.dev/api/v1/schedules/{schedule_id} \\\n\t--header 'Authorization: Bearer <token>' \\\n\t--header 'Content-Type: application/json' \\\n\t--data '{\"task\":\"my-updated-task\",\"cron\":\"0 0 * * *\"}'"
}
]
},
"delete": {
"operationId": "delete_schedule_v1",
"description": "Delete a schedule by its ID.",
"parameters": [
{
"in": "path",
"name": "schedule_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the schedule."
}
],
"responses": {
"200": {
"description": "Schedule deleted successfully"
},
"401": {
"description": "Unauthorized request"
},
"404": {
"description": "Resource not found"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nawait schedules.del(scheduleId);"
},
{
"lang": "sh",
"source": "curl --request DELETE \\\n\t--url https://api.trigger.dev/api/v1/schedules/{schedule_id} \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
},
"/api/v1/schedules/{schedule_id}/deactivate": {
"post": {
"operationId": "deactivate_schedule_v1",
"description": "Deactivate a schedule by its ID.",
"parameters": [
{
"in": "path",
"name": "schedule_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the schedule."
}
],
"responses": {
"200": {
"description": "Schedule updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleObject"
}
}
}
},
"401": {
"description": "Unauthorized request"
},
"404": {
"description": "Resource not found"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nconst schedule = await schedules.deactivate(scheduleId);"
},
{
"lang": "sh",
"source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/schedules/{schedule_id}/deactivate \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
},
"/api/v1/schedules/{schedule_id}/activate": {
"post": {
"operationId": "activate_schedule_v1",
"description": "Activate a schedule by its ID.",
"parameters": [
{
"in": "path",
"name": "schedule_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the schedule."
}
],
"responses": {
"200": {
"description": "Schedule updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleObject"
}
}
}
},
"401": {
"description": "Unauthorized request"
},
"404": {
"description": "Resource not found"
}
},
"tags": [
"schedules"
],
"security": [
{
"bearerAuth": []
}
],
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { schedules } from \"@trigger.dev/sdk/v3\";\n\nconst schedule = await schedules.activate(scheduleId);"
},
{
"lang": "sh",
"source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/schedules/{schedule_id}/activate \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
},
"/api/v1/runs/{run_id}/replay": {
"post": {
"description": "Creates a new run with the same payload and options as the original run.",
"parameters": [
{
"in": "path",
"name": "run_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of an existing run. When you trigger a run you will get an id in the response."
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The ID of the new run."
}
}
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or missing run ID",
"Failed to create new run"
]
}
}
}
}
}
},
"401": {
"description": "Unauthorized request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or Missing API key"
]
}
}
}
}
}
},
"404": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Run not found"
]
}
}
}
}
}
}
},
"tags": [
"run"
],
"security": [
{
"bearerAuth": []
}
],
"operationId": "replay_run_v1",
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nconst handle = await runs.replay(\"run_1234\");"
},
{
"lang": "sh",
"source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/runs/{run_id}/replay \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
},
"/api/v1/runs/{run_id}/cancel": {
"post": {
"description": "Cancels a run.",
"parameters": [
{
"in": "path",
"name": "run_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of an existing run. When you trigger a run you will get an id in the response."
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Confirmation message that the run was canceled."
}
}
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or missing run ID",
"Failed to create new run"
]
}
}
}
}
}
},
"401": {
"description": "Unauthorized request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or Missing API key"
]
}
}
}
}
}
},
"404": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Run not found"
]
}
}
}
}
}
}
},
"tags": [
"run"
],
"security": [
{
"bearerAuth": []
}
],
"operationId": "replay_run_v1",
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.cancel(\"run_1234\");"
},
{
"lang": "sh",
"source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/runs/{run_id}/cancel \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
},
"/api/v3/runs/{run_id}": {
"get": {
"description": "Retrieve a run",
"parameters": [
{
"in": "path",
"name": "run_id",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of an existing run. When you trigger a run you will get an id in the response."
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RetrieveRunResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or missing run ID"
]
}
}
}
}
}
},
"401": {
"description": "Unauthorized request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Invalid or Missing API key"
]
}
}
}
}
}
},
"404": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"enum": [
"Run not found"
]
}
}
}
}
}
}
},
"tags": [
"run"
],
"security": [
{
"bearerAuth": []
}
],
"operationId": "retrieve_run_v1",
"x-codeSamples": [
{
"lang": "typescript",
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.retrieve(\"run_1234\");"
},
{
"lang": "sh",
"source": "curl --request GET \\\n\t--url https://api.trigger.dev/api/v3/runs/{run_id} \\\n\t--header 'Authorization: Bearer <token>'"
}
]
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "Use your Secret API key in the form 'Bearer <SECRET KEY>' (without the quotation marks)"
}
},
"schemas": {
"RetrieveRunResponse": {
"type": "object",
"required": [
"id",
"status",
"taskIdentifier",
"createdAt",
"updatedAt",
"attempts"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED"
]
},
"taskIdentifier": {
"type": "string"
},
"idempotencyKey": {
"type": "string"
},
"version": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"attempts": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"status",
"createdAt",
"updatedAt"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"startedAt": {
"type": "string",
"format": "date-time"
},
"completedAt": {
"type": "string",
"format": "date-time"
}
}
}
}
}
},
"CreateScheduleOptions": {
"type": "object",
"properties": {
"task": {
"type": "string"
},
"cron": {
"type": "string"
},
"deduplicationKey": {
"type": "string"
},
"externalId": {
"type": "string"
}
},
"required": [
"task",
"cron"
]
},
"ScheduleObject": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "sched_1234",
"description": "The unique ID of the schedule, prefixed with 'sched_'"
},
"task": {
"type": "string",
"example": "my-scheduled-task",
"description": "The id of the scheduled task that will be triggered by this schedule"
},
"active": {
"type": "boolean",
"example": true,
"description": "Whether the schedule is active or not"
},
"deduplicationKey": {
"type": "string",
"example": "dedup_key_1234",
"description": "The deduplication key used to prevent creating duplicate schedules"
},
"externalId": {
"type": "string",
"example": "user_1234",
"description": "The external ID of the schedule. Can be anything that is useful to you (e.g., user ID, org ID, etc.)"
},
"generator": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"CRON"
]
},
"expression": {
"type": "string",
"description": "The cron expression used to generate the schedule",
"example": "0 0 * * *"
},
"description": {
"type": "string",
"description": "The description of the generator in plain english",
"example": "Every day at midnight"
}
}
},
"nextRun": {
"type": "string",
"format": "date-time",
"description": "The next time the schedule will run",
"example": "2024-04-01T00:00:00Z"
},
"environments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleEnvironment"
}
}
}
},
"ListSchedulesResult": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleObject"
}
},
"pagination": {
"type": "object",
"properties": {
"currentPage": {
"type": "integer"
},
"totalPages": {
"type": "integer"
},
"count": {
"type": "integer"
}
}
}
}
},
"ScheduleEnvironment": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"userName": {
"type": "string"
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
+1176
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
---
title: "Create Env Var"
openapi: "v3-openapi POST /api/v1/projects/{projectRef}/envvars/{env}"
---
+4
View File
@@ -0,0 +1,4 @@
---
title: "Delete Env Var"
openapi: "v3-openapi DELETE /api/v1/projects/{projectRef}/envvars/{env}/{name}"
---
+4
View File
@@ -0,0 +1,4 @@
---
title: "Import Env Vars"
openapi: "v3-openapi POST /api/v1/projects/{projectRef}/envvars/{env}/import"
---
+4
View File
@@ -0,0 +1,4 @@
---
title: "List Env Vars"
openapi: "v3-openapi GET /api/v1/projects/{projectRef}/envvars/{env}"
---
+4
View File
@@ -0,0 +1,4 @@
---
title: "Retrieve Env Var"
openapi: "v3-openapi GET /api/v1/projects/{projectRef}/envvars/{env}/{name}"
---
+4
View File
@@ -0,0 +1,4 @@
---
title: "Update Env Var"
openapi: "v3-openapi PUT /api/v1/projects/{projectRef}/envvars/{env}/{name}"
---
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/airtable
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/github
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -30,8 +30,8 @@
"@octokit/request-error": "^5.0.1",
"@octokit/webhooks": "^12.0.10",
"octokit": "^3.1.2",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"zod": "3.22.3"
},
"engines": {
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/linear
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"zod": "3.22.3"
},
"engines": {
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/slack
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -42,8 +42,8 @@
},
"dependencies": {
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34"
},
"engines": {
"node": ">=18.0.0"
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/plain
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"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:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/replicate
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "Trigger.dev integration for replicate",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/resend
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"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:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"resend": "^2.1.0"
},
"engines": {
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/sendgrid
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34"
},
"engines": {
"node": ">=16.8.0"
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/shopify
## 3.0.0-beta.34
### Patch Changes
- Updated dependencies [3a1b0c486]
- Updated dependencies [3f8b6d8fc]
- Updated dependencies [1281d40e4]
- @trigger.dev/sdk@3.0.0-beta.34
- @trigger.dev/integration-kit@3.0.0-beta.34
## 3.0.0-beta.33
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "3.0.0-beta.33",
"version": "3.0.0-beta.34",
"description": "Trigger.dev integration for @shopify/shopify-api",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@shopify/shopify-api": "^8.0.2",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.33",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.33",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.34",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.34",
"zod": "3.22.3"
},
"engines": {

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