Skip to content

Commit 2e9452a

Browse files
authored
feat: allow cancelling jobs from trigger-client sdk (#562)
* feat: allow cancelling jobs from trigger-client sdk * use presenter instead of service for non-mutating logic
1 parent 914745f commit 2e9452a

File tree

6 files changed

+176
-44
lines changed

6 files changed

+176
-44
lines changed

.changeset/orange-falcons-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
allow cancelling jobs from trigger-client
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Job } from "@trigger.dev/database";
2+
import { PrismaClient, prisma } from "~/db.server";
3+
4+
type ApiRunOptions = {
5+
runId: Job["id"];
6+
maxTasks?: number;
7+
taskDetails?: boolean;
8+
subTasks?: boolean;
9+
cursor?: string;
10+
};
11+
12+
export class ApiRunPresenter {
13+
#prismaClient: PrismaClient;
14+
15+
constructor(prismaClient: PrismaClient = prisma) {
16+
this.#prismaClient = prismaClient;
17+
}
18+
19+
public async call({
20+
runId,
21+
maxTasks = 20,
22+
taskDetails = false,
23+
subTasks = false,
24+
cursor,
25+
}: ApiRunOptions) {
26+
const take = Math.min(maxTasks, 50);
27+
28+
return await prisma.jobRun.findUnique({
29+
where: {
30+
id: runId,
31+
},
32+
select: {
33+
id: true,
34+
status: true,
35+
startedAt: true,
36+
updatedAt: true,
37+
completedAt: true,
38+
environmentId: true,
39+
output: true,
40+
tasks: {
41+
select: {
42+
id: true,
43+
parentId: true,
44+
displayKey: true,
45+
status: true,
46+
name: true,
47+
icon: true,
48+
startedAt: true,
49+
completedAt: true,
50+
params: taskDetails,
51+
output: taskDetails,
52+
},
53+
where: {
54+
parentId: subTasks ? undefined : null,
55+
},
56+
orderBy: {
57+
id: "asc",
58+
},
59+
take: take + 1,
60+
cursor: cursor
61+
? {
62+
id: cursor,
63+
}
64+
: undefined,
65+
},
66+
statuses: {
67+
select: { key: true, label: true, state: true, data: true, history: true },
68+
},
69+
},
70+
});
71+
}
72+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { ActionArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import { PrismaErrorSchema } from "~/db.server";
4+
import { z } from "zod";
5+
import { authenticateApiRequest } from "~/services/apiAuth.server";
6+
import { CancelRunService } from "~/services/runs/cancelRun.server";
7+
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
8+
9+
const ParamsSchema = z.object({
10+
runId: z.string(),
11+
});
12+
13+
export async function action({ request, params }: ActionArgs) {
14+
// Ensure this is a POST request
15+
if (request.method.toUpperCase() !== "POST") {
16+
return { status: 405, body: "Method Not Allowed" };
17+
}
18+
19+
// Authenticate the request
20+
const authenticationResult = await authenticateApiRequest(request);
21+
22+
if (!authenticationResult) {
23+
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
24+
}
25+
26+
const parsed = ParamsSchema.safeParse(params);
27+
28+
if (!parsed.success) {
29+
return json({ error: "Invalid or Missing runId" }, { status: 400 });
30+
}
31+
32+
const { runId } = parsed.data;
33+
34+
const service = new CancelRunService();
35+
try {
36+
await service.call({ runId });
37+
} catch (error) {
38+
const prismaError = PrismaErrorSchema.safeParse(error);
39+
// Record not found in the database
40+
if (prismaError.success && prismaError.data.code === "P2005") {
41+
return json({ error: "Run not found" }, { status: 404 });
42+
} else {
43+
return json({ error: "Internal Server Error" }, { status: 500 });
44+
}
45+
}
46+
47+
const presenter = new ApiRunPresenter();
48+
const jobRun = await presenter.call({
49+
runId: runId,
50+
});
51+
52+
if (!jobRun) {
53+
return json({ message: "Run not found" }, { status: 404 });
54+
}
55+
56+
return json({
57+
id: jobRun.id,
58+
status: jobRun.status,
59+
startedAt: jobRun.startedAt,
60+
updatedAt: jobRun.updatedAt,
61+
completedAt: jobRun.completedAt,
62+
output: jobRun.output,
63+
tasks: jobRun.tasks,
64+
statuses: jobRun.statuses.map((s) => ({
65+
...s,
66+
state: s.state ?? undefined,
67+
data: s.data ?? undefined,
68+
history: s.history ?? undefined,
69+
})),
70+
});
71+
}

apps/webapp/app/routes/api.v1.runs.$runId.ts

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { LoaderArgs } from "@remix-run/server-runtime";
22
import { json } from "@remix-run/server-runtime";
33
import { z } from "zod";
4-
import { prisma } from "~/db.server";
4+
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
55
import { authenticateApiRequest } from "~/services/apiAuth.server";
66
import { apiCors } from "~/utils/apiCors";
77
import { taskListToTree } from "~/utils/taskListToTree";
@@ -51,51 +51,15 @@ export async function loader({ request, params }: LoaderArgs) {
5151

5252
const query = parsedQuery.data;
5353
const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE";
54-
5554
const take = Math.min(query.take, 50);
5655

57-
const jobRun = await prisma.jobRun.findUnique({
58-
where: {
59-
id: runId,
60-
},
61-
select: {
62-
id: true,
63-
status: true,
64-
startedAt: true,
65-
updatedAt: true,
66-
completedAt: true,
67-
environmentId: true,
68-
output: true,
69-
tasks: {
70-
select: {
71-
id: true,
72-
parentId: true,
73-
displayKey: true,
74-
status: true,
75-
name: true,
76-
icon: true,
77-
startedAt: true,
78-
completedAt: true,
79-
params: showTaskDetails,
80-
output: showTaskDetails,
81-
},
82-
where: {
83-
parentId: query.subtasks ? undefined : null,
84-
},
85-
orderBy: {
86-
id: "asc",
87-
},
88-
take: take + 1,
89-
cursor: query.cursor
90-
? {
91-
id: query.cursor,
92-
}
93-
: undefined,
94-
},
95-
statuses: {
96-
select: { key: true, label: true, state: true, data: true, history: true },
97-
},
98-
},
56+
const presenter = new ApiRunPresenter();
57+
const jobRun = await presenter.call({
58+
runId: runId,
59+
maxTasks: take,
60+
taskDetails: showTaskDetails,
61+
subTasks: query.subtasks,
62+
cursor: query.cursor,
9963
});
10064

10165
if (!jobRun) {

packages/trigger-sdk/src/apiClient.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,22 @@ export class ApiClient {
399399
);
400400
}
401401

402+
async cancelRun(runId: string) {
403+
const apiKey = await this.#apiKey();
404+
405+
this.#logger.debug("Cancelling Run", {
406+
runId,
407+
});
408+
409+
return await zodfetch(GetRunSchema, `${this.#apiUrl}/api/v1/runs/${runId}/cancel`, {
410+
method: "POST",
411+
headers: {
412+
"Content-Type": "application/json",
413+
Authorization: `Bearer ${apiKey}`,
414+
},
415+
});
416+
}
417+
402418
async getRunStatuses(runId: string) {
403419
const apiKey = await this.#apiKey();
404420

packages/trigger-sdk/src/triggerClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,10 @@ export class TriggerClient {
642642
return this.#client.getRun(runId, options);
643643
}
644644

645+
async cancelRun(runId: string) {
646+
return this.#client.cancelRun(runId);
647+
}
648+
645649
async getRuns(jobSlug: string, options?: GetRunsOptions) {
646650
return this.#client.getRuns(jobSlug, options);
647651
}

0 commit comments

Comments
 (0)