Skip to content

Commit 7bf5a10

Browse files
committed
fix(webapp): the reports route exports only its loader
The route's schemas and helpers moved to reportsApi.server.ts — non-loader route exports that reach server-only modules fail the vite build (the e2e jobs' failure), which typecheck doesn't catch.
1 parent 3c22104 commit 7bf5a10

3 files changed

Lines changed: 69 additions & 55 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* The reports API route's schemas and helpers, out of the route module: a Remix
3+
* route may only export `loader`/`action`/`headers` alongside client-safe code,
4+
* and these depend on server-only modules (the vite build rejects the route
5+
* otherwise). The route and the tests both import from here.
6+
*/
7+
import { json } from "@remix-run/server-runtime";
8+
import { ReportFormatSchema, ReportPeriodSchema } from "@trigger.dev/core/v3/schemas";
9+
import { z } from "zod";
10+
import { reportQueryTables } from "~/presenters/v3/reports/report-registry";
11+
import { renderReportAnsi, renderReportMarkdown } from "~/presenters/v3/reports/renderMarkdown";
12+
import { type ReportViewModel } from "~/presenters/v3/reports/report-view-model";
13+
import { everyResource } from "~/services/routeBuilders/apiBuilder.server";
14+
15+
export const ReportParamsSchema = z.object({
16+
key: z.string(),
17+
});
18+
19+
/**
20+
* `period` and `format` come from `@trigger.dev/core/v3/schemas` — the same definitions the API
21+
* clients and the CLI use, so the accepted grammar can't drift between them. Note `period`
22+
* rejects seconds: reports bucket by whole minutes.
23+
*/
24+
export const ReportSearchParamsSchema = z.object({
25+
period: ReportPeriodSchema.optional(),
26+
// markdown (default) for CLI/MCP · json (the raw VM) for web · ansi for a colour terminal.
27+
format: ReportFormatSchema.default("markdown"),
28+
});
29+
30+
export type ReportFormatParam = z.infer<typeof ReportFormatSchema>;
31+
32+
/** Render the view model in the requested encoding, with the matching content type. */
33+
export function reportResponse(vm: ReportViewModel, format: ReportFormatParam): Response {
34+
switch (format) {
35+
case "json":
36+
return json(vm, { status: 200 });
37+
case "ansi":
38+
return new Response(renderReportAnsi(vm), {
39+
status: 200,
40+
headers: { "Content-Type": "text/plain; charset=utf-8" },
41+
});
42+
case "markdown":
43+
return new Response(renderReportMarkdown(vm), {
44+
status: 200,
45+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
46+
});
47+
}
48+
}
49+
50+
/**
51+
* Authorize per-table (like api.v1.query.ts) rather than the permissive
52+
* `{ type: "query", id: "all" }`: a JWT must be scoped to every table the *selected* report
53+
* reads, so a token scoped to only some tables can't fetch a report that reads others. The
54+
* tables come from the registry entry, so a narrower report gets a narrower check for free.
55+
*/
56+
export function reportAuthResource(key: string) {
57+
return everyResource(reportQueryTables(key).map((id) => ({ type: "query", id })));
58+
}

apps/webapp/app/routes/api.v1.reports.$key.ts

Lines changed: 10 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,18 @@
11
import { json } from "@remix-run/server-runtime";
2-
import { ReportFormatSchema, ReportPeriodSchema } from "@trigger.dev/core/v3/schemas";
3-
import { z } from "zod";
42
import { ReportPresenter } from "~/presenters/v3/reports/ReportPresenter.server";
3+
import { isReportKey, REPORT_KEYS } from "~/presenters/v3/reports/report-registry";
54
import {
6-
isReportKey,
7-
REPORT_KEYS,
8-
reportQueryTables,
9-
} from "~/presenters/v3/reports/report-registry";
10-
import { renderReportAnsi, renderReportMarkdown } from "~/presenters/v3/reports/renderMarkdown";
11-
import { type ReportViewModel } from "~/presenters/v3/reports/report-view-model";
5+
ReportParamsSchema,
6+
ReportSearchParamsSchema,
7+
reportAuthResource,
8+
reportResponse,
9+
} from "~/presenters/v3/reports/reportsApi.server";
1210
import { logger } from "~/services/logger.server";
13-
import { createLoaderApiRoute, everyResource } from "~/services/routeBuilders/apiBuilder.server";
14-
15-
export const ReportParamsSchema = z.object({
16-
key: z.string(),
17-
});
18-
19-
/**
20-
* `period` and `format` come from `@trigger.dev/core/v3/schemas` — the same definitions the API
21-
* clients and the CLI use, so the accepted grammar can't drift between them. Note `period`
22-
* rejects seconds: reports bucket by whole minutes.
23-
*/
24-
export const ReportSearchParamsSchema = z.object({
25-
period: ReportPeriodSchema.optional(),
26-
// markdown (default) for CLI/MCP · json (the raw VM) for web · ansi for a colour terminal.
27-
format: ReportFormatSchema.default("markdown"),
28-
});
29-
30-
export type ReportFormatParam = z.infer<typeof ReportFormatSchema>;
31-
32-
/** Render the view model in the requested encoding, with the matching content type. */
33-
export function reportResponse(vm: ReportViewModel, format: ReportFormatParam): Response {
34-
switch (format) {
35-
case "json":
36-
return json(vm, { status: 200 });
37-
case "ansi":
38-
return new Response(renderReportAnsi(vm), {
39-
status: 200,
40-
headers: { "Content-Type": "text/plain; charset=utf-8" },
41-
});
42-
case "markdown":
43-
return new Response(renderReportMarkdown(vm), {
44-
status: 200,
45-
headers: { "Content-Type": "text/markdown; charset=utf-8" },
46-
});
47-
}
48-
}
49-
50-
/**
51-
* Authorize per-table (like api.v1.query.ts) rather than the permissive
52-
* `{ type: "query", id: "all" }`: a JWT must be scoped to every table the *selected* report
53-
* reads, so a token scoped to only some tables can't fetch a report that reads others. The
54-
* tables come from the registry entry, so a narrower report gets a narrower check for free.
55-
*/
56-
export function reportAuthResource(key: string) {
57-
return everyResource(reportQueryTables(key).map((id) => ({ type: "query", id })));
58-
}
11+
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
5912

13+
// Only `loader` may be exported here: the route's schemas and helpers live in
14+
// reportsApi.server.ts, or the vite build flags server code reachable from a
15+
// non-loader export.
6016
export const loader = createLoaderApiRoute(
6117
{
6218
params: ReportParamsSchema,

apps/webapp/test/reportsApiRoute.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
reportAuthResource,
1111
reportResponse,
1212
ReportSearchParamsSchema,
13-
} from "~/routes/api.v1.reports.$key";
13+
} from "~/presenters/v3/reports/reportsApi.server";
1414

1515
// `everyResource(...)` tags its payload with this Symbol.for marker (see apiBuilder.server.ts),
1616
// so a test can read back exactly which resources the route will require.

0 commit comments

Comments
 (0)