From b383e4af54676b7ab2571f166187a235a369e7a8 Mon Sep 17 00:00:00 2001 From: hmacr Date: Thu, 5 Oct 2023 11:23:45 +0530 Subject: [PATCH 1/2] feat: allow cancelling jobs from trigger-client sdk --- .changeset/orange-falcons-smile.md | 5 ++ .../app/routes/api.v1.runs.$runId.cancel.ts | 71 +++++++++++++++++++ apps/webapp/app/routes/api.v1.runs.$runId.ts | 54 +++----------- .../webapp/app/services/runs/getRun.server.ts | 70 ++++++++++++++++++ packages/trigger-sdk/src/apiClient.ts | 16 +++++ packages/trigger-sdk/src/triggerClient.ts | 4 ++ 6 files changed, 175 insertions(+), 45 deletions(-) create mode 100644 .changeset/orange-falcons-smile.md create mode 100644 apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts create mode 100644 apps/webapp/app/services/runs/getRun.server.ts diff --git a/.changeset/orange-falcons-smile.md b/.changeset/orange-falcons-smile.md new file mode 100644 index 00000000000..e6cce81107a --- /dev/null +++ b/.changeset/orange-falcons-smile.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": minor +--- + +allow cancelling jobs from trigger-client diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts new file mode 100644 index 00000000000..9a4a7200a86 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts @@ -0,0 +1,71 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { PrismaErrorSchema } from "~/db.server"; +import { z } from "zod"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { CancelRunService } from "~/services/runs/cancelRun.server"; +import { GetRunInput, GetRunService } from "~/services/runs/getRun.server"; + +const ParamsSchema = z.object({ + runId: z.string(), +}); + +export async function action({ request, params }: ActionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + // Authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API Key" }, { status: 401 }); + } + + const parsed = ParamsSchema.safeParse(params); + + if (!parsed.success) { + return json({ error: "Invalid or Missing runId" }, { status: 400 }); + } + + const { runId } = parsed.data; + + const cancelRunService = new CancelRunService(); + try { + await cancelRunService.call({ runId }); + } catch (error) { + const prismaError = PrismaErrorSchema.safeParse(error); + // Record not found in the database + if (prismaError.success && prismaError.data.code === "P2005") { + return json({ error: "Run not found" }, { status: 404 }); + } else { + return json({ error: "Internal Server Error" }, { status: 500 }); + } + } + + const getRunService = new GetRunService(); + const jobRun = await getRunService.call({ + runId: runId, + } as GetRunInput); + + if (!jobRun) { + return json({ message: "Run not found" }, { status: 404 }); + } + + return json({ + id: jobRun.id, + status: jobRun.status, + startedAt: jobRun.startedAt, + updatedAt: jobRun.updatedAt, + completedAt: jobRun.completedAt, + output: jobRun.output, + tasks: jobRun.tasks, + statuses: jobRun.statuses.map((s) => ({ + ...s, + state: s.state ?? undefined, + data: s.data ?? undefined, + history: s.history ?? undefined, + })), + }); +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.ts index e1273654f82..2be5b6f91b4 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.ts @@ -1,8 +1,8 @@ import type { LoaderArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { z } from "zod"; -import { prisma } from "~/db.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { GetRunInput, GetRunService } from "~/services/runs/getRun.server"; import { apiCors } from "~/utils/apiCors"; import { taskListToTree } from "~/utils/taskListToTree"; @@ -51,52 +51,16 @@ export async function loader({ request, params }: LoaderArgs) { const query = parsedQuery.data; const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE"; - const take = Math.min(query.take, 50); - const jobRun = await prisma.jobRun.findUnique({ - where: { - id: runId, - }, - select: { - id: true, - status: true, - startedAt: true, - updatedAt: true, - completedAt: true, - environmentId: true, - output: true, - tasks: { - select: { - id: true, - parentId: true, - displayKey: true, - status: true, - name: true, - icon: true, - startedAt: true, - completedAt: true, - params: showTaskDetails, - output: showTaskDetails, - }, - where: { - parentId: query.subtasks ? undefined : null, - }, - orderBy: { - id: "asc", - }, - take: take + 1, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, - }, - statuses: { - select: { key: true, label: true, state: true, data: true, history: true }, - }, - }, - }); + const service = new GetRunService(); + const jobRun = await service.call({ + runId: runId, + maxTasks: take, + taskDetails: showTaskDetails, + subTasks: query.subtasks, + cursor: query.cursor, + } as GetRunInput); if (!jobRun) { return apiCors(request, json({ message: "Run not found" }, { status: 404 })); diff --git a/apps/webapp/app/services/runs/getRun.server.ts b/apps/webapp/app/services/runs/getRun.server.ts new file mode 100644 index 00000000000..d12886d5689 --- /dev/null +++ b/apps/webapp/app/services/runs/getRun.server.ts @@ -0,0 +1,70 @@ +import { PrismaClient, prisma } from "~/db.server"; +import { z } from "zod"; + +const GetRunInputSchema = z.object({ + runId: z.string(), + maxTasks: z.number().default(20), + taskDetails: z.boolean().default(false), + subTasks: z.boolean().default(false), + cursor: z.string().optional(), +}); + +export type GetRunInput = z.infer; + +export class GetRunService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(input: GetRunInput) { + const parsedInput = GetRunInputSchema.parse(input); + + const take = Math.min(parsedInput.maxTasks, 50); + + return await prisma.jobRun.findUnique({ + where: { + id: input.runId, + }, + select: { + id: true, + status: true, + startedAt: true, + updatedAt: true, + completedAt: true, + environmentId: true, + output: true, + tasks: { + select: { + id: true, + parentId: true, + displayKey: true, + status: true, + name: true, + icon: true, + startedAt: true, + completedAt: true, + params: parsedInput.taskDetails, + output: parsedInput.taskDetails, + }, + where: { + parentId: parsedInput.subTasks ? undefined : null, + }, + orderBy: { + id: "asc", + }, + take: take + 1, + cursor: parsedInput.cursor + ? { + id: parsedInput.cursor, + } + : undefined, + }, + statuses: { + select: { key: true, label: true, state: true, data: true, history: true }, + }, + }, + }); + } +} diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index 42a2976a52f..ad003b10ac6 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -399,6 +399,22 @@ export class ApiClient { ); } + async cancelRun(runId: string) { + const apiKey = await this.#apiKey(); + + this.#logger.debug("Cancelling Run", { + runId, + }); + + return await zodfetch(GetRunSchema, `${this.#apiUrl}/api/v1/runs/${runId}/cancel`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + }); + } + async getRunStatuses(runId: string) { const apiKey = await this.#apiKey(); diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index 55412d89b40..b1766f16429 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -642,6 +642,10 @@ export class TriggerClient { return this.#client.getRun(runId, options); } + async cancelRun(runId: string) { + return this.#client.cancelRun(runId); + } + async getRuns(jobSlug: string, options?: GetRunsOptions) { return this.#client.getRuns(jobSlug, options); } From b1b91758a9b869fbda8e872a0930da06747b290e Mon Sep 17 00:00:00 2001 From: hmacr Date: Thu, 5 Oct 2023 18:49:20 +0530 Subject: [PATCH 2/2] use presenter instead of service for non-mutating logic --- .changeset/orange-falcons-smile.md | 2 +- .../ApiRunPresenter.server.ts} | 44 ++++++++++--------- .../app/routes/api.v1.runs.$runId.cancel.ts | 12 ++--- apps/webapp/app/routes/api.v1.runs.$runId.ts | 8 ++-- 4 files changed, 34 insertions(+), 32 deletions(-) rename apps/webapp/app/{services/runs/getRun.server.ts => presenters/ApiRunPresenter.server.ts} (58%) diff --git a/.changeset/orange-falcons-smile.md b/.changeset/orange-falcons-smile.md index e6cce81107a..7651f714f3b 100644 --- a/.changeset/orange-falcons-smile.md +++ b/.changeset/orange-falcons-smile.md @@ -1,5 +1,5 @@ --- -"@trigger.dev/sdk": minor +"@trigger.dev/sdk": patch --- allow cancelling jobs from trigger-client diff --git a/apps/webapp/app/services/runs/getRun.server.ts b/apps/webapp/app/presenters/ApiRunPresenter.server.ts similarity index 58% rename from apps/webapp/app/services/runs/getRun.server.ts rename to apps/webapp/app/presenters/ApiRunPresenter.server.ts index d12886d5689..6e8ccd3d2b5 100644 --- a/apps/webapp/app/services/runs/getRun.server.ts +++ b/apps/webapp/app/presenters/ApiRunPresenter.server.ts @@ -1,31 +1,33 @@ +import { Job } from "@trigger.dev/database"; import { PrismaClient, prisma } from "~/db.server"; -import { z } from "zod"; -const GetRunInputSchema = z.object({ - runId: z.string(), - maxTasks: z.number().default(20), - taskDetails: z.boolean().default(false), - subTasks: z.boolean().default(false), - cursor: z.string().optional(), -}); +type ApiRunOptions = { + runId: Job["id"]; + maxTasks?: number; + taskDetails?: boolean; + subTasks?: boolean; + cursor?: string; +}; -export type GetRunInput = z.infer; - -export class GetRunService { +export class ApiRunPresenter { #prismaClient: PrismaClient; constructor(prismaClient: PrismaClient = prisma) { this.#prismaClient = prismaClient; } - public async call(input: GetRunInput) { - const parsedInput = GetRunInputSchema.parse(input); - - const take = Math.min(parsedInput.maxTasks, 50); + public async call({ + runId, + maxTasks = 20, + taskDetails = false, + subTasks = false, + cursor, + }: ApiRunOptions) { + const take = Math.min(maxTasks, 50); return await prisma.jobRun.findUnique({ where: { - id: input.runId, + id: runId, }, select: { id: true, @@ -45,19 +47,19 @@ export class GetRunService { icon: true, startedAt: true, completedAt: true, - params: parsedInput.taskDetails, - output: parsedInput.taskDetails, + params: taskDetails, + output: taskDetails, }, where: { - parentId: parsedInput.subTasks ? undefined : null, + parentId: subTasks ? undefined : null, }, orderBy: { id: "asc", }, take: take + 1, - cursor: parsedInput.cursor + cursor: cursor ? { - id: parsedInput.cursor, + id: cursor, } : undefined, }, diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts index 9a4a7200a86..876d22d8465 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts @@ -4,7 +4,7 @@ import { PrismaErrorSchema } from "~/db.server"; import { z } from "zod"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { CancelRunService } from "~/services/runs/cancelRun.server"; -import { GetRunInput, GetRunService } from "~/services/runs/getRun.server"; +import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; const ParamsSchema = z.object({ runId: z.string(), @@ -31,9 +31,9 @@ export async function action({ request, params }: ActionArgs) { const { runId } = parsed.data; - const cancelRunService = new CancelRunService(); + const service = new CancelRunService(); try { - await cancelRunService.call({ runId }); + await service.call({ runId }); } catch (error) { const prismaError = PrismaErrorSchema.safeParse(error); // Record not found in the database @@ -44,10 +44,10 @@ export async function action({ request, params }: ActionArgs) { } } - const getRunService = new GetRunService(); - const jobRun = await getRunService.call({ + const presenter = new ApiRunPresenter(); + const jobRun = await presenter.call({ runId: runId, - } as GetRunInput); + }); if (!jobRun) { return json({ message: "Run not found" }, { status: 404 }); diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.ts index 2be5b6f91b4..12fbaffc9a0 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.ts @@ -1,8 +1,8 @@ import type { LoaderArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { z } from "zod"; +import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; -import { GetRunInput, GetRunService } from "~/services/runs/getRun.server"; import { apiCors } from "~/utils/apiCors"; import { taskListToTree } from "~/utils/taskListToTree"; @@ -53,14 +53,14 @@ export async function loader({ request, params }: LoaderArgs) { const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE"; const take = Math.min(query.take, 50); - const service = new GetRunService(); - const jobRun = await service.call({ + const presenter = new ApiRunPresenter(); + const jobRun = await presenter.call({ runId: runId, maxTasks: take, taskDetails: showTaskDetails, subTasks: query.subtasks, cursor: query.cursor, - } as GetRunInput); + }); if (!jobRun) { return apiCors(request, json({ message: "Run not found" }, { status: 404 }));