Skip to content

Commit e6a6845

Browse files
committed
feat: add createActionPATApiRoute builder and use it for set-default-region
1 parent b1ed301 commit e6a6845

3 files changed

Lines changed: 409 additions & 71 deletions

File tree

Lines changed: 36 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
21
import { json } from "@remix-run/server-runtime";
32
import { tryCatch } from "@trigger.dev/core/utils";
43
import { z } from "zod";
54
import { prisma } from "~/db.server";
65
import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server";
7-
import { logger } from "~/services/logger.server";
8-
import { authorizePatOrganizationAccess } from "~/services/organizationApiAccess.server";
9-
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
10-
import { ServiceValidationError } from "~/v3/services/baseService.server";
6+
import { createActionPATApiRoute } from "~/services/routeBuilders/apiBuilder.server";
117
import { SetDefaultRegionService } from "~/v3/services/setDefaultRegion.server";
128

139
const ParamsSchema = z.object({
@@ -19,106 +15,75 @@ const SetDefaultRegionRequestBody = z.object({
1915
region: z.string().min(1),
2016
});
2117

22-
export async function action({ request, params }: ActionFunctionArgs) {
23-
// Clearing the default is unsupported: SetDefaultRegionService has no path to
24-
// unset defaultWorkerGroupId, so DELETE is intentionally not implemented.
25-
if (request.method.toUpperCase() !== "PUT") {
26-
return json({ error: "Method Not Allowed" }, { status: 405 });
27-
}
28-
29-
const parsedParams = ParamsSchema.safeParse(params);
30-
31-
if (!parsedParams.success) {
32-
return json({ error: "Invalid Params" }, { status: 400 });
33-
}
34-
35-
try {
36-
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
37-
38-
if (!authenticationResult) {
39-
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
40-
}
41-
18+
// Clearing the default is unsupported: SetDefaultRegionService has no path to
19+
// unset defaultWorkerGroupId, so only PUT is implemented (other methods 405).
20+
export const action = createActionPATApiRoute(
21+
{
22+
method: "PUT",
23+
params: ParamsSchema,
24+
body: SetDefaultRegionRequestBody,
25+
// Resolve the org (id only, no membership) so the plugin can compute the
26+
// caller's role floor for the manage:project gate below.
27+
context: async ({ projectRef }) => {
28+
const project = await prisma.project.findFirst({
29+
where: { externalRef: projectRef, deletedAt: null },
30+
select: { organizationId: true },
31+
});
32+
return project ? { organizationId: project.organizationId } : {};
33+
},
34+
authorization: { action: "manage", resource: () => ({ type: "project" }) },
35+
},
36+
async ({ params, body, authentication }) => {
37+
// Membership floor: resolve scoped to the caller so a non-member gets a 404
38+
// (the authorization block enforces the role; this enforces membership).
4239
const project = await prisma.project.findFirst({
4340
where: {
44-
externalRef: parsedParams.data.projectRef,
41+
externalRef: params.projectRef,
4542
organization: {
4643
deletedAt: null,
47-
members: { some: { userId: authenticationResult.userId } },
44+
members: { some: { userId: authentication.userId } },
4845
},
4946
deletedAt: null,
5047
},
51-
select: { id: true, slug: true, organizationId: true },
48+
select: { id: true, slug: true },
5249
});
5350

5451
if (!project) {
5552
return json({ error: "Project not found" }, { status: 404 });
5653
}
5754

58-
const denied = await authorizePatOrganizationAccess({
59-
request,
60-
organizationId: project.organizationId,
61-
resource: "project",
62-
action: "manage",
63-
});
64-
if (denied) {
65-
return denied;
66-
}
67-
68-
let rawBody: unknown;
69-
try {
70-
rawBody = await request.json();
71-
} catch {
72-
return json({ error: "Invalid request body" }, { status: 400 });
73-
}
74-
75-
const body = SetDefaultRegionRequestBody.safeParse(rawBody);
76-
77-
if (!body.success) {
78-
return json({ error: "Invalid request body" }, { status: 400 });
79-
}
80-
8155
// Resolve the region name to a worker group id the same way the dashboard
8256
// does — through the presenter, which filters to regions this project can
8357
// actually use (allowed queues / hidden / compute access). PAT users are
8458
// never admins here.
8559
const presenter = new RegionsPresenter();
8660
const [presenterError, result] = await tryCatch(
87-
presenter.call({ userId: authenticationResult.userId, projectSlug: project.slug })
61+
presenter.call({ userId: authentication.userId, projectSlug: project.slug })
8862
);
8963

9064
if (presenterError) {
9165
return json({ error: presenterError.message }, { status: 400 });
9266
}
9367

94-
const region = result.regions.find((r) => r.name === body.data.region);
68+
const region = result.regions.find((r) => r.name === body.region);
9569

9670
if (!region) {
9771
return json(
9872
{
99-
error: `Region '${body.data.region}' not found`,
73+
error: `Region '${body.region}' not found`,
10074
availableRegions: result.regions.map((r) => r.name),
10175
},
10276
{ status: 400 }
10377
);
10478
}
10579

106-
try {
107-
const updated = await new SetDefaultRegionService().call({
108-
projectId: project.id,
109-
regionId: region.id,
110-
});
80+
// SetDefaultRegionService throws ServiceValidationError; the builder maps it
81+
// to its status (default 400).
82+
const updated = await new SetDefaultRegionService().call({
83+
projectId: project.id,
84+
regionId: region.id,
85+
});
11186

112-
return json({ id: updated.id, name: updated.name });
113-
} catch (error) {
114-
if (error instanceof ServiceValidationError) {
115-
return json({ error: error.message }, { status: 400 });
116-
}
117-
throw error;
118-
}
119-
} catch (error) {
120-
if (error instanceof Response) throw error;
121-
logger.error("Failed to set default region", { error });
122-
return json({ error: "Internal Server Error" }, { status: 500 });
87+
return json({ id: updated.id, name: updated.name });
12388
}
124-
}
89+
);

0 commit comments

Comments
 (0)