Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/trpc-controller-input-pipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@croco/protocols-trpc": patch
"@croco/problems-core": patch
---

Run controller and method pipes on tRPC body, path, query, and header arguments inside the request lifecycle, preserving context arguments and reporting pipe failures through the existing Problem/filter pipeline.

Keep generated tRPC Problem diagnostic source locations synchronized with the pipe-enabled request lifecycle.
8 changes: 4 additions & 4 deletions docs/problem-code-registry.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "croco.problem-code-registry.v1",
"problemCount": 981,
Expand Down Expand Up @@ -22536,7 +22536,7 @@
"sources": [
{
"file": "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
"line": 92,
"line": 99,
"column": 1,
"kind": "problem-class"
}
Expand Down Expand Up @@ -22566,7 +22566,7 @@
"sources": [
{
"file": "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
"line": 216,
"line": 224,
"column": 13,
"kind": "problem-factory"
}
Expand Down Expand Up @@ -22596,7 +22596,7 @@
"sources": [
{
"file": "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
"line": 76,
"line": 83,
"column": 1,
"kind": "problem-class"
}
Expand Down Expand Up @@ -22686,7 +22686,7 @@
"sources": [
{
"file": "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
"line": 63,
"line": 70,
"column": 1,
"kind": "problem-class"
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14513,7 +14513,7 @@ Sources:

Sources:

- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:92:1` (problem-class)
- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:99:1` (problem-class)

<a id="protocols-trpc-execution-context-missing"></a>

Expand All @@ -14531,7 +14531,7 @@ Sources:

Sources:

- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:216:13` (problem-factory)
- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:224:13` (problem-factory)

<a id="protocols-trpc-provider-container-required"></a>

Expand All @@ -14549,7 +14549,7 @@ Sources:

Sources:

- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:76:1` (problem-class)
- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:83:1` (problem-class)

<a id="protocols-trpc-request-normalization-failed"></a>

Expand Down Expand Up @@ -14603,7 +14603,7 @@ Sources:

Sources:

- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:63:1` (problem-class)
- `packages/protocols-trpc/src/libs/createTrpcRouter.ts:70:1` (problem-class)

<a id="ratelimit-core-duplicate-default-policy"></a>

Expand Down
8 changes: 4 additions & 4 deletions packages/problems-core/src/generated/problem-code-registry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import type { TypedProblemDetails } from "../libs/Problem";
import type { ProblemCodeRegistry } from "../libs/ProblemRegistry";

Expand Down Expand Up @@ -23461,7 +23461,7 @@
sources: [
{
file: "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
line: 92,
line: 99,
column: 1,
kind: "problem-class",
},
Expand Down Expand Up @@ -23494,7 +23494,7 @@
sources: [
{
file: "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
line: 216,
line: 224,
column: 13,
kind: "problem-factory",
},
Expand Down Expand Up @@ -23527,7 +23527,7 @@
sources: [
{
file: "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
line: 76,
line: 83,
column: 1,
kind: "problem-class",
},
Expand Down Expand Up @@ -23625,7 +23625,7 @@
sources: [
{
file: "packages/protocols-trpc/src/libs/createTrpcRouter.ts",
line: 63,
line: 70,
column: 1,
kind: "problem-class",
},
Expand Down
11 changes: 11 additions & 0 deletions packages/protocols-trpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ The location schemas run before the controller method. `@Ctx()` and `@Raw()` bot
unmodified tRPC procedure context and are not client input. For HTTP adapters, include the raw
request or other transport envelope fields in `createContext` when a controller needs them.

`@UsePipes` runs class pipes before method pipes on each body, path, query, or header argument,
inside the interceptor-wrapped handler and the Croco request DI boundary. Async transformations
are awaited; pipe failures reach the existing Problem/filter pipeline. Pipes receive REST
argument metadata (`body`, `param`, `query`, or `header`, plus the parameter name when declared).
Context and authentication arguments are preserved.

tRPC validates its input schema before running these pipes. HTTP runs route pipes before
parameter-level validation pipes, while contract-bound path/query schemas are parsed before
route pipes. A tRPC pipe therefore receives schema-parsed input and cannot repair input that
the tRPC schema rejects. The existing body-only and location-envelope shapes are preserved.

## Verification

```bash
Expand Down
47 changes: 42 additions & 5 deletions packages/protocols-trpc/src/libs/createTrpcRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import type { RequestContext } from "@croco/framework-context";
import { Problem, ProblemCategory, ProblemFactory } from "@croco/problems-core";
import type { RouteContractSourceLocation, RouteIR } from "@croco/protocols-core";
import { extractRouteIR } from "@croco/protocols-core";
import { getFilters, getGuards, getInterceptors, type Constructor } from "@croco/protocols-rest";
import {
getFilters,
getGuards,
getInterceptors,
getPipes,
type Constructor,
type PipeTransform,
} from "@croco/protocols-rest";
import {
type AnyProcedure,
type AnyRouter,
Expand Down Expand Up @@ -170,6 +177,7 @@ function createProcedure(
const guardProviders = getGuards(controller, route.methodName);
const filterProviders = getFilters(controller, route.methodName);
const interceptorProviders = getInterceptors(controller, route.methodName);
const pipeProviders = getPipes(controller, route.methodName);
const createFilters = (): TrpcPipelineConfig["filters"] =>
filterProviders.map((provider) => instantiateProvider(provider, options));
const createGuards = (): TrpcPipelineConfig["guards"] =>
Expand Down Expand Up @@ -221,7 +229,14 @@ function createProcedure(

return executionPipeline.runInterceptors(
context,
async () => callRoute(controllerInstance, route, input, ctx),
async () =>
callRoute(
controllerInstance,
route,
input,
ctx,
pipeProviders.map((provider) => instantiateProvider(provider, options)),
),
createInterceptors(),
);
};
Expand All @@ -233,19 +248,41 @@ function createProcedure(
return procedure.mutation(resolver);
}

function callRoute(
async function callRoute(
controllerInstance: object,
route: RouteIR,
input: unknown,
context: unknown,
): unknown {
pipes: readonly PipeTransform[],
): Promise<unknown> {
const handler = Reflect.get(controllerInstance, route.methodName);

if (!isRouteHandler(handler)) {
throw new TrpcRouteHandlerError(route.methodName);
}

return handler.apply(controllerInstance, resolveTrpcRouteParams(route, input, context));
const args = resolveTrpcRouteParams(route, input, context);
for (const [position, param] of route.params.entries()) {
if (
param.kind !== "body" &&
param.kind !== "path" &&
param.kind !== "query" &&
param.kind !== "header"
) {
continue;
}

const index = param.index ?? position;
const metadata = {
type: param.kind === "path" ? ("param" as const) : param.kind,
...(param.name ? { name: param.name } : {}),
};
for (const pipe of pipes) {
args[index] = await pipe.transform(args[index], metadata);
}
}

return handler.apply(controllerInstance, args);
}

function getDomainName(route: RouteIR): string {
Expand Down
Loading
Loading