diff --git a/.changeset/orange-eggs-fold.md b/.changeset/orange-eggs-fold.md new file mode 100644 index 000000000..77dd02b3f --- /dev/null +++ b/.changeset/orange-eggs-fold.md @@ -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 diff --git a/apps/webapp/app/routes/api.v2.events.$eventId.ts b/apps/webapp/app/routes/api.v2.events.$eventId.ts new file mode 100644 index 000000000..213ca8626 --- /dev/null +++ b/apps/webapp/app/routes/api.v2.events.$eventId.ts @@ -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>>; + +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, + }, + }, + }); +} diff --git a/apps/webapp/app/routes/api.v2.runs.$runId.statuses.ts b/apps/webapp/app/routes/api.v2.runs.$runId.statuses.ts new file mode 100644 index 000000000..c3dfada96 --- /dev/null +++ b/apps/webapp/app/routes/api.v2.runs.$runId.statuses.ts @@ -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 })); + } +} diff --git a/apps/webapp/app/routes/api.v2.runs.$runId.ts b/apps/webapp/app/routes/api.v2.runs.$runId.ts new file mode 100644 index 000000000..ebdfbf365 --- /dev/null +++ b/apps/webapp/app/routes/api.v2.runs.$runId.ts @@ -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, + }) + ); +} diff --git a/packages/react/src/events.ts b/packages/react/src/events.ts index c240515b2..8fd4e4826 100644 --- a/packages/react/src/events.ts +++ b/packages/react/src/events.ts @@ -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}`, diff --git a/packages/react/src/runs.ts b/packages/react/src/runs.ts index 1cd1bee13..379154f81 100644 --- a/packages/react/src/runs.ts +++ b/packages/react/src/runs.ts @@ -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( { diff --git a/packages/react/src/statuses.ts b/packages/react/src/statuses.ts index ec722d664..032fea885 100644 --- a/packages/react/src/statuses.ts +++ b/packages/react/src/statuses.ts @@ -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}`, diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index 5581c430c..c487691f7 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -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}`, diff --git a/references/nextjs-reference/package.json b/references/nextjs-reference/package.json index 5e0b11963..c4784a10c 100644 --- a/references/nextjs-reference/package.json +++ b/references/nextjs-reference/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/references/nextjs-reference/src/jobs/hooks.ts b/references/nextjs-reference/src/jobs/hooks.ts index 09ed9412c..c029ebeba 100644 --- a/references/nextjs-reference/src/jobs/hooks.ts +++ b/references/nextjs-reference/src/jobs/hooks.ts @@ -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",