Add runs.retrieve management API method to get info about a run by run ID

This commit is contained in:
Eric Allam
2024-05-04 22:24:42 +01:00
parent 1670c4c41d
commit 203e002087
11 changed files with 511 additions and 26 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Add runs.retrieve management API method to get info about a run by run ID
@@ -0,0 +1,114 @@
import { AttemptStatus, RetrieveRunResponse, RunStatus, logger } from "@trigger.dev/core/v3";
import { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import assertNever from "assert-never";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { BasePresenter } from "./basePresenter.server";
export class ApiRetrieveRunPresenter extends BasePresenter {
public async call(
friendlyId: string,
env: AuthenticatedEnvironment,
showSecretDetails: boolean
): Promise<RetrieveRunResponse | undefined> {
return this.traceWithEnv("call", env, async (span) => {
const taskRun = await this._prisma.taskRun.findUnique({
where: {
friendlyId,
runtimeEnvironmentId: env.id,
},
include: {
attempts: {
orderBy: {
createdAt: "desc",
},
},
lockedToVersion: true,
},
});
if (!taskRun) {
logger.debug("Task run not found", { friendlyId, envId: env.id });
return undefined;
}
return {
id: taskRun.friendlyId,
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(taskRun.status),
taskIdentifier: taskRun.taskIdentifier,
idempotencyKey: taskRun.idempotencyKey ?? undefined,
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
createdAt: taskRun.createdAt ?? undefined,
updatedAt: taskRun.updatedAt ?? undefined,
attempts: !showSecretDetails
? []
: taskRun.attempts.map((a) => ({
id: a.friendlyId,
status: ApiRetrieveRunPresenter.apiStatusFromAttemptStatus(a.status),
createdAt: a.createdAt ?? undefined,
updatedAt: a.updatedAt ?? undefined,
startedAt: a.startedAt ?? undefined,
completedAt: a.completedAt ?? undefined,
})),
};
});
}
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
switch (status) {
case "WAITING_FOR_DEPLOY":
case "PENDING": {
return "PENDING";
}
case "RETRYING_AFTER_FAILURE":
case "EXECUTING": {
return "EXECUTING";
}
case "WAITING_TO_RESUME":
case "PAUSED": {
return "PAUSED";
}
case "CANCELED": {
return "CANCELED";
}
case "COMPLETED_SUCCESSFULLY": {
return "COMPLETED";
}
case "SYSTEM_FAILURE":
case "INTERRUPTED":
case "CRASHED":
case "COMPLETED_WITH_ERRORS": {
return "FAILED";
}
default: {
assertNever(status);
}
}
}
static apiStatusFromAttemptStatus(status: TaskRunAttemptStatus): AttemptStatus {
switch (status) {
case "PENDING": {
return "PENDING";
}
case "PAUSED": {
return "PAUSED";
}
case "EXECUTING": {
return "EXECUTING";
}
case "COMPLETED": {
return "COMPLETED";
}
case "FAILED": {
return "FAILED";
}
case "CANCELED": {
return "CANCELED";
}
default: {
assertNever(status);
}
}
}
}
@@ -0,0 +1,44 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { apiCors } from "~/utils/apiCors";
const ParamsSchema = z.object({
runId: z.string(),
});
export async function loader({ request, params }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
const authenticationResult = await authenticateApiRequest(request, {
allowPublicKey: true,
});
if (!authenticationResult) {
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
}
const authenticatedEnv = authenticationResult.environment;
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return apiCors(request, json({ error: "Invalid or missing runId" }, { status: 400 }));
}
const { runId } = parsed.data;
const showSecretDetails = authenticationResult.type === "PRIVATE";
const presenter = new ApiRetrieveRunPresenter();
const result = await presenter.call(runId, authenticatedEnv, showSecretDetails);
if (!result) {
return apiCors(request, json({ error: "Run not found" }, { status: 404 }));
}
return apiCors(request, json(result));
}
+71 -22
View File
@@ -1,8 +1,14 @@
{
"$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.json"
],
"versions": [
"v3 (Developer Preview)",
"v2"
],
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png",
@@ -90,7 +96,9 @@
{
"group": "",
"version": "v3 (Developer Preview)",
"pages": ["v3/introduction"]
"pages": [
"v3/introduction"
]
},
{
"group": "Getting Started",
@@ -113,7 +121,12 @@
"v3/apikeys",
{
"group": "Task types",
"pages": ["v3/tasks-regular", "v3/tasks-scheduled", "v3/tasks-zod", "v3/tasks-webhooks"]
"pages": [
"v3/tasks-regular",
"v3/tasks-scheduled",
"v3/tasks-zod",
"v3/tasks-webhooks"
]
},
"v3/trigger-config"
]
@@ -121,7 +134,10 @@
{
"group": "Development",
"version": "v3 (Developer Preview)",
"pages": ["v3/cli-dev", "v3/run-tests"]
"pages": [
"v3/cli-dev",
"v3/run-tests"
]
},
{
"group": "Deployment",
@@ -132,7 +148,9 @@
"v3/github-actions",
{
"group": "Deployment integrations",
"pages": ["v3/vercel-integration"]
"pages": [
"v3/vercel-integration"
]
}
]
},
@@ -208,7 +226,9 @@
},
{
"group": "Objects",
"pages": ["v3/reference-context"]
"pages": [
"v3/reference-context"
]
},
{
"group": "CLI",
@@ -226,9 +246,7 @@
{
"group": "Runs API",
"pages": [
"v3/management-start-run",
"v3/management-get-run",
"v3/management-get-runs",
"v3/management-retrieve-run",
"v3/management-replay-run",
"v3/management-cancel-run"
]
@@ -259,7 +277,11 @@
{
"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": "Help",
@@ -460,7 +482,10 @@
"pages": [
{
"group": "Airtable",
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
"pages": [
"integrations/apis/airtable",
"integrations/apis/airtable-tasks"
]
},
{
"group": "GitHub",
@@ -486,16 +511,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",
@@ -507,7 +541,10 @@
},
{
"group": "Slack",
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
"pages": [
"integrations/apis/slack",
"integrations/apis/slack-tasks"
]
},
"integrations/apis/stripe",
{
@@ -532,7 +569,9 @@
"sdk/triggerclient/constructor",
{
"group": "Instance properties",
"pages": ["sdk/triggerclient/store"]
"pages": [
"sdk/triggerclient/store"
]
},
{
"group": "Instance methods",
@@ -595,7 +634,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -606,7 +648,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -618,7 +663,9 @@
},
{
"group": "HTTP Reference",
"pages": ["sdk/api-reference/events/create-an-event"]
"pages": [
"sdk/api-reference/events/create-an-event"
]
},
{
"group": "React SDK",
@@ -633,7 +680,9 @@
{
"group": "Overview",
"version": "v2",
"pages": ["examples/introduction"]
"pages": [
"examples/introduction"
]
}
],
"footerSocials": {
@@ -641,4 +690,4 @@
"github": "https://github.com/triggerdotdev",
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
}
}
}
+191 -2
View File
@@ -493,7 +493,7 @@
"x-codeSamples": [
{
"lang": "typescript",
"source": "const handle = await runs.replay(existingRun.id);"
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nconst handle = await runs.replay(\"run_1234\");"
},
{
"lang": "sh",
@@ -601,7 +601,7 @@
"x-codeSamples": [
{
"lang": "typescript",
"source": "await runs.cancel(existingRun.id);"
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.cancel(\"run_1234\");"
},
{
"lang": "sh",
@@ -609,6 +609,107 @@
}
]
}
},
"/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": {
@@ -620,6 +721,94 @@
}
},
"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": {
+4
View File
@@ -0,0 +1,4 @@
---
title: "Retrieve run"
openapi: "v3-openapi GET /api/v3/runs/{run_id}"
---
+13
View File
@@ -11,6 +11,7 @@ import {
ListScheduleOptions,
ListSchedulesResult,
ReplayRunResponse,
RetrieveRunResponse,
ScheduleObject,
TaskRunExecutionResult,
TriggerTaskRequestBody,
@@ -133,6 +134,18 @@ export class ApiClient {
);
}
retrieveRun(runId: string) {
return zodfetch(
RetrieveRunResponse,
`${this.baseUrl}/api/v3/runs/${runId}`,
{
method: "GET",
headers: this.#getHeaders(false),
},
zodFetchOptions
);
}
replayRun(runId: string) {
return zodfetch(
ReplayRunResponse,
+46
View File
@@ -316,3 +316,49 @@ export const ListScheduleOptions = z.object({
});
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
export const RunStatus = z.enum([
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED",
]);
export type RunStatus = z.infer<typeof RunStatus>;
export const AttemptStatus = z.enum([
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED",
]);
export type AttemptStatus = z.infer<typeof AttemptStatus>;
export const RetrieveRunResponse = z.object({
id: z.string(),
status: RunStatus,
taskIdentifier: z.string(),
idempotencyKey: z.string().optional(),
version: z.string().optional(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
attempts: z.array(
z
.object({
id: z.string(),
status: AttemptStatus,
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
startedAt: z.coerce.date().optional(),
completedAt: z.coerce.date().optional(),
})
.optional()
),
});
export type RetrieveRunResponse = z.infer<typeof RetrieveRunResponse>;
+1 -1
View File
@@ -50,7 +50,7 @@ export const TaskRunInternalError = z.object({
"TASK_RUN_CANCELLED",
"TASK_OUTPUT_ERROR",
"HANDLE_ERROR_ERROR",
"GRACEFUL_EXIT_TIMEOUT"
"GRACEFUL_EXIT_TIMEOUT",
]),
message: z.string().optional(),
});
+17 -1
View File
@@ -1,11 +1,27 @@
import { CanceledRunResponse, ReplayRunResponse, apiClientManager } from "@trigger.dev/core/v3";
import {
CanceledRunResponse,
ReplayRunResponse,
RetrieveRunResponse,
apiClientManager,
} from "@trigger.dev/core/v3";
import { apiClientMissingError } from "./shared";
export const runs = {
replay: replayRun,
cancel: cancelRun,
retrieve: retrieveRun,
};
async function retrieveRun(runId: string): Promise<RetrieveRunResponse> {
const apiClient = apiClientManager.client;
if (!apiClient) {
throw apiClientMissingError();
}
return await apiClient.retrieveRun(runId);
}
async function replayRun(runId: string): Promise<ReplayRunResponse> {
const apiClient = apiClientManager.client;
+4
View File
@@ -8,6 +8,10 @@ dotenv.config();
export async function run() {
try {
const run = await simpleChildTask.trigger({ message: "Hello, World!" });
const retrievedRun = await runs.retrieve(run.id);
console.log("retrieved run", retrievedRun);
const canceled = await runs.cancel(run.id);
console.log("canceled run", canceled);