Fix api run statuses (#874)

* Added a subtask for testing

* Make it easier to run the CLI from the nextjs reference project

* Copies of the run and statuses endpoints, but without simplifying the run statuses

* Use the new v2 endpoints that give the full run statuses

* Removed unused import

* v2 events endpoint with the full run status info

* Changeset
This commit is contained in:
Matt Aitken
2024-01-29 11:42:19 +00:00
committed by GitHub
parent af485b9180
commit dd879c8e4a
10 changed files with 286 additions and 9 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/react": patch
---
Updated run, run statuses and event endpoints to v2 to get full run statuses
@@ -0,0 +1,84 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { GetEvent } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { apiCors } from "~/utils/apiCors";
const ParamsSchema = z.object({
eventId: 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 eventId" }, { status: 400 }));
}
const { eventId } = parsed.data;
const event = await findEventRecord(eventId, authenticatedEnv.id);
if (!event) {
return apiCors(request, json({ error: "Event not found" }, { status: 404 }));
}
return apiCors(request, json(toJSON(event)));
}
function toJSON(eventRecord: FoundEventRecord): GetEvent {
return {
id: eventRecord.eventId,
name: eventRecord.name,
createdAt: eventRecord.createdAt,
updatedAt: eventRecord.updatedAt,
runs: eventRecord.runs.map((run) => ({
id: run.id,
status: run.status,
startedAt: run.startedAt,
completedAt: run.completedAt,
})),
};
}
type FoundEventRecord = NonNullable<Awaited<ReturnType<typeof findEventRecord>>>;
async function findEventRecord(eventId: string, environmentId: string) {
return await prisma.eventRecord.findUnique({
select: {
eventId: true,
name: true,
createdAt: true,
updatedAt: true,
runs: {
select: {
id: true,
status: true,
startedAt: true,
completedAt: true,
},
},
},
where: {
eventId_environmentId: {
eventId,
environmentId,
},
},
});
}
@@ -0,0 +1,82 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { apiCors } from "~/utils/apiCors";
const ParamsSchema = z.object({
runId: z.string(),
});
const RecordsSchema = z.array(JobRunStatusRecordSchema);
export async function loader({ request, params }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request, { allowPublicKey: true });
if (!authenticationResult) {
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
}
const { runId } = ParamsSchema.parse(params);
logger.debug("Get run statuses", {
runId,
});
try {
const run = await prisma.jobRun.findUnique({
where: {
id: runId,
},
select: {
id: true,
status: true,
output: true,
statuses: {
orderBy: {
createdAt: "asc",
},
},
},
});
if (!run) {
return apiCors(request, json({ error: `No run found for id ${runId}` }, { status: 404 }));
}
const parsedStatuses = RecordsSchema.parse(
run.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
}))
);
return apiCors(
request,
json({
run: {
id: run.id,
status: run.status,
output: run.output,
},
statuses: parsedStatuses,
})
);
} catch (error) {
if (error instanceof Error) {
return apiCors(request, json({ error: error.message }, { status: 400 }));
}
return apiCors(request, json({ error: "Something went wrong" }, { status: 500 }));
}
}
@@ -0,0 +1,100 @@
import type { LoaderFunctionArgs } 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 { apiCors } from "~/utils/apiCors";
import { taskListToTree } from "~/utils/taskListToTree";
const ParamsSchema = z.object({
runId: z.string(),
});
const SearchQuerySchema = z.object({
cursor: z.string().optional(),
take: z.coerce.number().default(20),
subtasks: z.coerce.boolean().default(false),
taskdetails: z.coerce.boolean().default(false),
});
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 url = new URL(request.url);
const parsedQuery = SearchQuerySchema.safeParse(Object.fromEntries(url.searchParams));
if (!parsedQuery.success) {
return apiCors(
request,
json({ error: "Invalid or missing query parameters" }, { status: 400 })
);
}
const query = parsedQuery.data;
const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE";
const take = Math.min(query.take, 50);
const presenter = new ApiRunPresenter();
const jobRun = await presenter.call({
runId: runId,
maxTasks: take,
taskDetails: showTaskDetails,
subTasks: query.subtasks,
cursor: query.cursor,
});
if (!jobRun) {
return apiCors(request, json({ message: "Run not found" }, { status: 404 }));
}
if (jobRun.environmentId !== authenticatedEnv.id) {
return apiCors(request, json({ message: "Run not found" }, { status: 404 }));
}
const selectedTasks = jobRun.tasks.slice(0, take);
const tasks = taskListToTree(selectedTasks, query.subtasks);
const nextTask = jobRun.tasks[take];
return apiCors(
request,
json({
id: jobRun.id,
status: jobRun.status,
startedAt: jobRun.startedAt,
updatedAt: jobRun.updatedAt,
completedAt: jobRun.completedAt,
output: jobRun.output,
tasks: tasks.map((task) => {
const { parentId, ...rest } = task;
return { ...rest };
}),
statuses: jobRun.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
})),
nextCursor: nextTask ? nextTask.id : undefined,
})
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ export function useEventDetails(eventId: string | undefined): UseEventDetailsRes
{
queryKey: [`triggerdotdev-event-${eventId}`],
queryFn: async () => {
return await zodfetch(GetEventSchema, `${apiUrl}/api/v1/events/${eventId}`, {
return await zodfetch(GetEventSchema, `${apiUrl}/api/v2/events/${eventId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${publicApiKey}`,
+1 -1
View File
@@ -24,7 +24,7 @@ export function useRunDetails(
const { refreshIntervalMs: refreshInterval, ...otherOptions } = options || {};
const url = urlWithSearchParams(`${apiUrl}/api/v1/runs/${runId}`, otherOptions);
const url = urlWithSearchParams(`${apiUrl}/api/v2/runs/${runId}`, otherOptions);
return useQuery(
{
+1 -1
View File
@@ -49,7 +49,7 @@ export function useRunStatuses(
{
queryKey: [`triggerdotdev-run-${runId}`],
queryFn: async () => {
return await zodfetch(GetRunStatusesSchema, `${apiUrl}/api/v1/runs/${runId}/statuses`, {
return await zodfetch(GetRunStatusesSchema, `${apiUrl}/api/v2/runs/${runId}/statuses`, {
method: "GET",
headers: {
Authorization: `Bearer ${publicApiKey}`,
+3 -3
View File
@@ -450,7 +450,7 @@ export class ApiClient {
eventId,
});
return await zodfetch(GetEventSchema, `${this.#apiUrl}/api/v1/events/${eventId}`, {
return await zodfetch(GetEventSchema, `${this.#apiUrl}/api/v2/events/${eventId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
@@ -467,7 +467,7 @@ export class ApiClient {
return await zodfetch(
GetRunSchema,
urlWithSearchParams(`${this.#apiUrl}/api/v1/runs/${runId}`, options),
urlWithSearchParams(`${this.#apiUrl}/api/v2/runs/${runId}`, options),
{
method: "GET",
headers: {
@@ -500,7 +500,7 @@ export class ApiClient {
runId,
});
return await zodfetch(GetRunStatusesSchema, `${this.#apiUrl}/api/v1/runs/${runId}/statuses`, {
return await zodfetch(GetRunStatusesSchema, `${this.#apiUrl}/api/v2/runs/${runId}/statuses`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
+3 -2
View File
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"generate:types": "npx supabase gen types typescript --project-id axtbanoixaztvdntngew --schema public --schema public_2 > src/supabase.types.ts"
"generate:types": "npx supabase gen types typescript --project-id axtbanoixaztvdntngew --schema public --schema public_2 > src/supabase.types.ts",
"dev:trigger": "trigger-cli dev --port 3000"
},
"dependencies": {
"@trigger.dev/eslint-plugin": "workspace:*",
@@ -43,4 +44,4 @@
"trigger.dev": {
"endpointId": "nextjs-example"
}
}
}
@@ -14,6 +14,10 @@ client.defineJob({
// state: "loading",
});
await io.runTask("task-1", async () => {
await io.wait("wait-subtask", 2);
});
await io.wait("wait-input", 2);
await gettingInputData.update("input-data-complete", {
@@ -54,7 +58,7 @@ client.defineJob({
},
});
await io.wait("wait-again", 4);
await io.wait("wait-again-2", 4);
await generatingMemes.update("completed-generation", {
label: "Generated memes",