diff --git a/src/app/api/portfolio/[portfolioId]/collaborators/route.test.ts b/src/app/api/portfolio/[portfolioId]/collaborators/inviteRequestSchema.test.ts similarity index 94% rename from src/app/api/portfolio/[portfolioId]/collaborators/route.test.ts rename to src/app/api/portfolio/[portfolioId]/collaborators/inviteRequestSchema.test.ts index 7b855f3f..e332c555 100644 --- a/src/app/api/portfolio/[portfolioId]/collaborators/route.test.ts +++ b/src/app/api/portfolio/[portfolioId]/collaborators/inviteRequestSchema.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { inviteRequestSchema } from "./route"; +import { inviteRequestSchema } from "./inviteRequestSchema"; describe("inviteRequestSchema", () => { it("accepts an invite request with just email and role (no name)", () => { diff --git a/src/app/api/portfolio/[portfolioId]/collaborators/inviteRequestSchema.ts b/src/app/api/portfolio/[portfolioId]/collaborators/inviteRequestSchema.ts new file mode 100644 index 00000000..12526c67 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/collaborators/inviteRequestSchema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; +import { ROLE_OPTIONS } from "@/app/portfolio/[slug]/(portfolio)/settings/roles"; + +// Body schema for POST /api/portfolio/[portfolioId]/collaborators. Lives in +// its own file because Next.js 15 rejects non-handler named exports from +// route.ts. See route.test.ts for the contract tests. +export const inviteRequestSchema = z.object({ + email: z.string().email(), + role: z.enum(ROLE_OPTIONS), +}); diff --git a/src/app/api/portfolio/[portfolioId]/collaborators/route.ts b/src/app/api/portfolio/[portfolioId]/collaborators/route.ts index 8fa8ca8d..4db4432b 100644 --- a/src/app/api/portfolio/[portfolioId]/collaborators/route.ts +++ b/src/app/api/portfolio/[portfolioId]/collaborators/route.ts @@ -29,6 +29,7 @@ import { denyIfNotAdmin, resolvePortfolioPrivilege, } from "@/app/lib/resolvePortfolioPrivilege"; +import { inviteRequestSchema } from "./inviteRequestSchema"; // Get collaborators (users) that have access to the portfolio, plus the // effective privilege of the requesting user (so the UI knows which actions @@ -192,13 +193,6 @@ export async function PUT( } } -// Body schema for the invite endpoint. Exported so the contract can be -// unit-tested directly — see route.test.ts. -export const inviteRequestSchema = z.object({ - email: z.string().email(), - role: z.enum(ROLE_OPTIONS), -}); - // POST: invite a user by email. // // Unified flow: in nearly every case we write a pending portfolio_invitations