Adding a procedure
A new endpoint, from router to test to client call.
Pick a builder
Every procedure starts from a builder in backend/src/trpc/trpc.ts. Each one adds a check on top of the one before.
| Builder | Lets through |
|---|---|
publicProcedure | Anyone |
protectedProcedure | Anyone signed in. ctx.session is non-null |
acceptedTermsProcedure | Signed in and has accepted the active terms |
requirePermission("key") | All of the above, plus the role has that feature |
requireOrganizationPermission("key") | Signed in, and has that feature in the organization named by input.organizationId |
Every builder logs the call, so you never add logging by hand.
Write it
Add a file in backend/src/trpc/routers/. Validate input with Zod:
import { z } from "zod";
import { db } from "../../db/index.js";
import { notes } from "../../db/schema.js";
import { protectedProcedure, requirePermission, router } from "../trpc.js";
export const notesRouter = router({
list: protectedProcedure.query(() => db.select().from(notes)),
create: requirePermission("notes.create")
.input(z.object({ body: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const [note] = await db.insert(notes).values({ body: input.body, authorId: ctx.session.user.id }).returning();
return note;
}),
});
Take the user's id from ctx.session, never from input. A client-supplied id lets anyone act as anyone.
Register it
Add it to the root router in backend/src/trpc/router.ts:
export const appRouter = router({
// ...
notes: notesRouter,
});
The apps get the types with no extra step: trpc.notes.create.mutate({ body }).
Test it
Put the test beside the router. createCallerFactory calls procedures directly, with no HTTP:
import { beforeEach, expect, it } from "vitest";
import { contextFor, createTestUser } from "../../test/context.js";
import { resetDb } from "../../test/reset-db.js";
import { createCallerFactory } from "../trpc.js";
import { notesRouter } from "./notes.js";
const createCaller = createCallerFactory(notesRouter);
beforeEach(async () => {
await resetDb();
});
it("refuses a role without notes.create", async () => {
const user = await createTestUser({ role: "standard" });
const caller = createCaller(contextFor(user));
await expect(caller.create({ body: "hi" })).rejects.toMatchObject({ code: "FORBIDDEN" });
});
Run it with yarn test. See Testing.
Before you open the pull request
A new procedure usually means a new feature key. Run through the four permission questions in Checking permissions.