Skip to content

Commit

Permalink
feat(server): support Valibot for validation (#4670)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabian-hiller committed Aug 3, 2023
1 parent 176c0db commit 49c7b53
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 24 deletions.
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
"tslib": "^2.5.0",
"tsx": "^3.12.7",
"typescript": "^5.1.3",
"valibot": "^0.8.0",
"vitest": "^0.32.0",
"ws": "^8.0.0",
"yup": "^1.0.0",
Expand Down
11 changes: 6 additions & 5 deletions packages/server/src/core/internals/getParseFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@ export function getParseFn<TType>(procedureParser: Parser): ParseFn<TType> {
const parser = procedureParser as any;

if (typeof parser === 'function') {
// ProcedureParserCustomValidatorEsque
// ParserCustomValidatorEsque
return parser;
}

if (typeof parser.parseAsync === 'function') {
// ProcedureParserZodEsque
// ParserZodEsque
return parser.parseAsync.bind(parser);
}

if (typeof parser.parse === 'function') {
// ProcedureParserZodEsque
// ParserZodEsque
// ParserValibotEsque
return parser.parse.bind(parser);
}

if (typeof parser.validateSync === 'function') {
// ProcedureParserYupEsque
// ParserYupEsque
return parser.validateSync.bind(parser);
}

if (typeof parser.create === 'function') {
// ProcedureParserSuperstructEsque
// ParserSuperstructEsque
return parser.create.bind(parser);
}

Expand Down
14 changes: 10 additions & 4 deletions packages/server/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ export type ParserZodEsque<TInput, TParsedInput> = {
_output: TParsedInput;
};

export type ParserValibotEsque<TInput, TParsedInput> = {
types?: {
input: TInput;
output: TParsedInput;
};
};

export type ParserMyZodEsque<TInput> = {
parse: (input: any) => TInput;
};
Expand Down Expand Up @@ -30,10 +37,9 @@ export type ParserWithoutInput<TInput> =
| ParserSuperstructEsque<TInput>
| ParserYupEsque<TInput>;

export type ParserWithInputOutput<TInput, TParsedInput> = ParserZodEsque<
TInput,
TParsedInput
>;
export type ParserWithInputOutput<TInput, TParsedInput> =
| ParserZodEsque<TInput, TParsedInput>
| ParserValibotEsque<TInput, TParsedInput>;

export type Parser = ParserWithInputOutput<any, any> | ParserWithoutInput<any>;

Expand Down
1 change: 1 addition & 0 deletions packages/tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"superstruct": "^1.0.0",
"tsx": "^3.12.7",
"typescript": "^5.1.3",
"valibot": "^0.8.0",
"vite": "^4.1.2",
"vitest": "^0.32.0",
"vitest-environment-miniflare": "^2.12.0",
Expand Down
99 changes: 99 additions & 0 deletions packages/tests/server/outputParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { routerToServerAndClientNew } from './___testHelpers';
import { initTRPC } from '@trpc/server/src';
import myzod from 'myzod';
import * as t from 'superstruct';
import * as v from 'valibot';
import * as yup from 'yup';
import { z } from 'zod';

Expand Down Expand Up @@ -102,6 +103,104 @@ test('zod transform', async () => {
await close();
});

test('valibot', async () => {
const trpc = initTRPC.create();
const router = trpc.router({
q: trpc.procedure
.input(v.union([v.string(), v.number()]))
.output(
v.object({
input: v.string(),
}),
)
.query(({ input }) => {
return { input: input as string };
}),
});
const { proxy, close } = routerToServerAndClientNew(router);

const output = await proxy.q.query('foobar');
expectTypeOf(output.input).toBeString();
expect(output).toMatchInlineSnapshot(`
Object {
"input": "foobar",
}
`);

await expect(proxy.q.query(1234)).rejects.toMatchInlineSnapshot(
`[TRPCClientError: Output validation failed]`,
);

await close();
});

test('valibot async', async () => {
const trpc = initTRPC.create();
const router = trpc.router({
q: trpc.procedure
.input(v.unionAsync([v.stringAsync(), v.numberAsync()]))
.output(
v.objectAsync(
{
input: v.stringAsync(),
},
[v.customAsync<{ input: string }>(async (value) => !!value)],
),
)
.query(({ input }) => {
return { input: input as string };
}),
});

const { proxy, close } = routerToServerAndClientNew(router);

const output = await proxy.q.query('foobar');
expectTypeOf(output.input).toBeString();
expect(output).toMatchInlineSnapshot(`
Object {
"input": "foobar",
}
`);

await expect(proxy.q.query(1234)).rejects.toMatchInlineSnapshot(
`[TRPCClientError: Output validation failed]`,
);

await close();
});

test('valibot transform', async () => {
const trpc = initTRPC.create();
const router = trpc.router({
q: trpc.procedure
.input(v.union([v.string(), v.number()]))
.output(
v.object({
input: v.transform(v.string(), (s) => s.length),
}),
)
.query(({ input }) => {
return { input: input as string };
}),
});

const { proxy, close } = routerToServerAndClientNew(router);

const output = await proxy.q.query('foobar');
expectTypeOf(output.input).toBeNumber();
expect(output).toMatchInlineSnapshot(`
Object {
"input": 6,
}
`);

await expect(proxy.q.query(1234)).rejects.toMatchInlineSnapshot(
`[TRPCClientError: Output validation failed]`,
);

await close();
});

test('superstruct', async () => {
const trpc = initTRPC.create();
const router = trpc.router({
Expand Down
86 changes: 86 additions & 0 deletions packages/tests/server/validators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import myzod from 'myzod';
import * as T from 'runtypes';
import * as $ from 'scale-codec';
import * as st from 'superstruct';
import * as v from 'valibot';
import * as yup from 'yup';
import { z } from 'zod';

Expand Down Expand Up @@ -134,6 +135,91 @@ test('zod transform mixed input/output', async () => {
await close();
});

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

const router = t.router({
num: t.procedure.input(v.number()).query(({ input }) => {
expectTypeOf(input).toBeNumber();
return {
input,
};
}),
});

const { close, proxy } = routerToServerAndClientNew(router);
const res = await proxy.num.query(123);

await expect(proxy.num.query('123' as any)).rejects.toMatchInlineSnapshot(
'[TRPCClientError: Invalid type]',
);
expect(res.input).toBe(123);
await close();
});

test('valibot async', async () => {
const t = initTRPC.create();
const input = v.stringAsync([
v.customAsync(async (value) => value === 'foo'),
]);

const router = t.router({
q: t.procedure.input(input).query(({ input }) => {
expectTypeOf(input).toBeString();
return {
input,
};
}),
});

const { close, proxy } = routerToServerAndClientNew(router);

await expect(proxy.q.query('bar')).rejects.toMatchInlineSnapshot(
'[TRPCClientError: Invalid input]',
);
const res = await proxy.q.query('foo');
expect(res).toMatchInlineSnapshot(`
Object {
"input": "foo",
}
`);
await close();
});

test('valibot transform mixed input/output', async () => {
const t = initTRPC.create();
const input = v.object({
length: v.transform(v.string(), (s) => s.length),
});

const router = t.router({
num: t.procedure.input(input).query(({ input }) => {
expectTypeOf(input.length).toBeNumber();
return {
input,
};
}),
});

const { close, proxy } = routerToServerAndClientNew(router);

await expect(proxy.num.query({ length: '123' })).resolves
.toMatchInlineSnapshot(`
Object {
"input": Object {
"length": 3,
},
}
`);

await expect(
// @ts-expect-error this should only accept a string
proxy.num.query({ length: 123 }),
).rejects.toMatchInlineSnapshot('[TRPCClientError: Invalid type]');

await close();
});

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

Expand Down
Loading

3 comments on commit 49c7b53

@vercel
Copy link

@vercel vercel bot commented on 49c7b53 Aug 3, 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-trpc.vercel.app
www-git-main-trpc.vercel.app
beta.trpc.io
www.trpc.io
trpc.io
alpha.trpc.io

@vercel
Copy link

@vercel vercel bot commented on 49c7b53 Aug 3, 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
next-prisma-starter-trpc.vercel.app
nextjs.trpc.io

@vercel
Copy link

@vercel vercel bot commented on 49c7b53 Aug 3, 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.trpc.io
og-image-git-main-trpc.vercel.app
og-image-three-neon.vercel.app

Please sign in to comment.