Skip to content

Commit

Permalink
feat: add ability to have optionals when chaining input parsers (#3797)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusmarminge committed Feb 20, 2023
1 parent a5ce5e1 commit 652708f
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 20 deletions.
29 changes: 9 additions & 20 deletions packages/server/src/core/internals/procedureBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { TRPCError } from '../../error/TRPCError';
import { getTRPCErrorFromUnknown } from '../../error/utils';
import {
InferOptional,
MaybePromise,
Simplify,
UndefinedKeys,
} from '../../types';
import { MaybePromise, Simplify } from '../../types';
import {
MiddlewareBuilder,
MiddlewareFunction,
Expand Down Expand Up @@ -68,19 +63,9 @@ export interface BuildProcedure<
: TParams
> {}

type Merge<TType, TWith> = {
[TKey in keyof TType | keyof TWith]: TKey extends keyof TType
? TKey extends keyof TWith
? TType[TKey] & TWith[TKey]
: TType[TKey]
: TWith[TKey & keyof TWith];
};

type OverwriteIfDefined<TType, TWith> = UnsetMarker extends TType
? TWith
: Simplify<
InferOptional<Merge<TType, TWith>, UndefinedKeys<Merge<TType, TWith>>>
>;
: Simplify<TType & TWith>;

type ErrorMessage<TMessage extends string> = TMessage;

Expand All @@ -104,9 +89,13 @@ export interface ProcedureBuilder<TParams extends ProcedureParams> {
input<$Parser extends Parser>(
schema: TParams['_input_out'] extends UnsetMarker
? $Parser
: inferParser<$Parser>['out'] extends Record<string, unknown>
? TParams['_input_out'] extends Record<string, unknown>
? $Parser
: inferParser<$Parser>['out'] extends Record<string, unknown> | undefined
? TParams['_input_out'] extends Record<string, unknown> | undefined
? undefined extends inferParser<$Parser>['out'] // if current is optional the previous must be too
? undefined extends TParams['_input_out']
? $Parser
: ErrorMessage<'Cannot chain an optional parser to a required parser'>
: $Parser
: ErrorMessage<'All input parsers did not resolve to an object'>
: ErrorMessage<'All input parsers did not resolve to an object'>,
): ProcedureBuilder<{
Expand Down
156 changes: 156 additions & 0 deletions packages/tests/server/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,146 @@ test('only allow double input validator for object-like inputs', () => {
}
});

describe('multiple input validators with optionals', () => {
const t = initTRPC.create();

const webhookProc = t.procedure.input(
z
.object({
id: z.string(),
eventTypeId: z.number().optional(),
})
.optional(),
);

test('2nd parser also optional => merged optional', async () => {
const webhookRouter = t.router({
byId: webhookProc
.input(
z
.object({
webhookId: z.string(),
})
.optional(),
)
.query(({ input }) => {
expectTypeOf(input).toEqualTypeOf<
| {
id: string;
eventTypeId?: number;
webhookId: string;
}
| undefined
>();
return input;
}),
});

const opts = routerToServerAndClientNew(webhookRouter);

await expect(opts.proxy.byId.query()).resolves.toBeUndefined();
await expect(opts.proxy.byId.query(undefined)).resolves.toBeUndefined();
await expect(
opts.proxy.byId.query({ id: '123', webhookId: '456' }),
).resolves.toMatchObject({
id: '123',
webhookId: '456',
});

await opts.close();
});

test('2nd parser required => merged required', async () => {
const webhookRouter = t.router({
byId: webhookProc
.input(
z.object({
webhookId: z.string(),
}),
)
.query(({ input }) => {
expectTypeOf(input).toEqualTypeOf<{
id: string;
eventTypeId?: number;
webhookId: string;
}>();
return input;
}),
});

const opts = routerToServerAndClientNew(webhookRouter);

await expect(
opts.proxy.byId.query({ id: '123', webhookId: '456' }),
).resolves.toMatchObject({
id: '123',
webhookId: '456',
});
// @ts-expect-error - missing id and webhookId
await expect(opts.proxy.byId.query()).rejects.toThrow();
// @ts-expect-error - missing id and webhookId
await expect(opts.proxy.byId.query(undefined)).rejects.toThrow();
await expect(
opts.proxy.byId.query({ id: '123', eventTypeId: 1, webhookId: '456' }),
).resolves.toMatchObject({
id: '123',
eventTypeId: 1,
webhookId: '456',
});

await opts.close();
});

test('with optional keys', async () => {
const webhookRouter = t.router({
byId: webhookProc
.input(
z.object({
webhookId: z.string(),
foo: z.string().optional(),
}),
)
.query(({ input }) => {
expectTypeOf(input).toEqualTypeOf<{
id: string;
eventTypeId?: number;
webhookId: string;
foo?: string;
}>();
return input;
}),
});

const opts = routerToServerAndClientNew(webhookRouter);
await expect(
opts.proxy.byId.query({ id: '123', webhookId: '456' }),
).resolves.toMatchObject({
id: '123',
webhookId: '456',
});
await expect(
opts.proxy.byId.query({ id: '123', webhookId: '456', foo: 'bar' }),
).resolves.toMatchObject({
id: '123',
webhookId: '456',
foo: 'bar',
});

await opts.close();
});

test('cannot chain optional to required', async () => {
try {
t.procedure
.input(z.object({ foo: z.string() }))
// @ts-expect-error cannot chain optional to required
.input(z.object({ bar: z.number() }).optional());
} catch {
// whatever
}
});
});

test('no input', async () => {
const t = initTRPC.create();

Expand Down Expand Up @@ -358,6 +498,12 @@ test('double validators with undefined', async () => {
return input;
});

type Input = inferProcedureParams<typeof proc>['_input_in'];
expectTypeOf<Input>().toEqualTypeOf<{
roomId: string;
optionalKey?: string;
}>();

const router = t.router({
proc,
});
Expand Down Expand Up @@ -388,6 +534,12 @@ test('double validators with undefined', async () => {
return input;
});

type Input = inferProcedureParams<typeof proc>['_input_in'];
expectTypeOf<Input>().toEqualTypeOf<{
roomId?: string;
key: string;
}>();

const router = t.router({
proc,
});
Expand Down Expand Up @@ -421,6 +573,10 @@ test('merges optional with required property', async () => {
.query(() => 'hi'),
});

type Input = inferProcedureInput<typeof router['proc']>;
// ^?
expectTypeOf<Input>().toEqualTypeOf<{ id: string }>();

const client = createTRPCProxyClient<typeof router>({
links: [],
});
Expand Down

3 comments on commit 652708f

@vercel
Copy link

@vercel vercel bot commented on 652708f Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

www – ./www

www-git-main-trpc.vercel.app
beta.trpc.io
www.trpc.io
www-trpc.vercel.app
alpha.trpc.io
trpc.io

@vercel
Copy link

@vercel vercel bot commented on 652708f Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

og-image – ./www/og-image

og-image-trpc.vercel.app
og-image-three-neon.vercel.app
og-image-git-main-trpc.vercel.app
og-image.trpc.io

@vercel
Copy link

@vercel vercel bot commented on 652708f Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-prisma-starter – ./examples/next-prisma-starter

next-prisma-starter-git-main-trpc.vercel.app
nextjs.trpc.io
next-prisma-starter-trpc.vercel.app

Please sign in to comment.