Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement: Add basic job metadata retrieval functionality to React hooks #891

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/empty-onions-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/react": patch
"@trigger.dev/core": patch
---

Add basic job metadata retrieval functionality to React hooks
5 changes: 5 additions & 0 deletions apps/webapp/app/presenters/ApiRunPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export class ApiRunPresenter {
completedAt: true,
environmentId: true,
output: true,
job: {
select: {
slug: true,
},
},
tasks: {
select: {
id: true,
Expand Down
92 changes: 92 additions & 0 deletions apps/webapp/app/routes/api.v3.events.$eventId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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,
job: {
id: run.job.slug,
},
})),
};
}

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,
job: {
select: {
slug: true,
},
},
},
},
},
where: {
eventId_environmentId: {
eventId,
environmentId,
},
},
});
}
90 changes: 90 additions & 0 deletions apps/webapp/app/routes/api.v3.runs.$runId.statuses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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,
job: {
select: {
slug: 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,
job: {
id: run.job.slug,
},
})
);
} 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 }));
}
}
103 changes: 103 additions & 0 deletions apps/webapp/app/routes/api.v3.runs.$runId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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,
})),
job: {
id: jobRun.job.slug,
},
nextCursor: nextTask ? nextTask.id : undefined,
})
);
}
1 change: 1 addition & 0 deletions packages/core/src/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ export const CreateExternalConnectionBodySchema = z.object({
export type CreateExternalConnectionBody = z.infer<typeof CreateExternalConnectionBodySchema>;

export const GetRunStatusesSchema = z.object({
job: z.object({ id: z.string() }),
run: z.object({ id: z.string(), status: RunStatusSchema, output: z.any().optional() }),
statuses: z.array(JobRunStatusRecordSchema),
});
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/schemas/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const GetEventSchema = z.object({
startedAt: z.coerce.date().optional().nullable(),
/** When the run completed */
completedAt: z.coerce.date().optional().nullable(),
/** The Job that the run associated with */
job: z.object({ id: z.string() }).optional().nullable(),
})
),
});
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/schemas/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const GetRunSchema = RunSchema.extend({
tasks: z.array(RunTaskWithSubtasksSchema),
/** Any status updates that were published from the run */
statuses: z.array(JobRunStatusRecordSchema).default([]),
/** The job that the run is associated with */
job: z.object({ id: z.string() }),
/** If there are more tasks, you can use this to get them */
nextCursor: z.string().optional(),
});
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useEventDetails(eventId: string | undefined): UseEventDetailsRes
{
queryKey: [`triggerdotdev-event-${eventId}`],
queryFn: async () => {
return await zodfetch(GetEventSchema, `${apiUrl}/api/v2/events/${eventId}`, {
return await zodfetch(GetEventSchema, `${apiUrl}/api/v3/events/${eventId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${publicApiKey}`,
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function useRunDetails(

const { refreshIntervalMs: refreshInterval, ...otherOptions } = options || {};

const url = urlWithSearchParams(`${apiUrl}/api/v2/runs/${runId}`, otherOptions);
const url = urlWithSearchParams(`${apiUrl}/api/v3/runs/${runId}`, otherOptions);

return useQuery(
{
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export type UseRunStatusesResult =
error: undefined;
statuses: undefined;
run: undefined;
job: undefined;
}
| {
fetchStatus: "error";
error: Error;
statuses: undefined;
run: undefined;
job: undefined;
}
| ({
fetchStatus: "success";
Expand All @@ -49,7 +51,7 @@ export function useRunStatuses(
{
queryKey: [`triggerdotdev-run-${runId}`],
queryFn: async () => {
return await zodfetch(GetRunStatusesSchema, `${apiUrl}/api/v2/runs/${runId}/statuses`, {
return await zodfetch(GetRunStatusesSchema, `${apiUrl}/api/v3/runs/${runId}/statuses`, {
method: "GET",
headers: {
Authorization: `Bearer ${publicApiKey}`,
Expand Down Expand Up @@ -78,6 +80,7 @@ export function useRunStatuses(
error: undefined,
statuses: undefined,
run: undefined,
job: undefined,
};
}
case "error": {
Expand All @@ -86,6 +89,7 @@ export function useRunStatuses(
error: queryResult.error,
statuses: undefined,
run: undefined,
job: undefined,
};
}
case "success": {
Expand All @@ -94,6 +98,7 @@ export function useRunStatuses(
error: undefined,
run: queryResult.data.run,
statuses: queryResult.data.statuses,
job: queryResult.data.job,
};
}
}
Expand Down