Skip to content

Commit

Permalink
Merge pull request #68 from lifeomic/input-modification
Browse files Browse the repository at this point in the history
feat!: support modifications during parsing of inputs
  • Loading branch information
swain authored Jun 8, 2023
2 parents 81e6a3a + a383067 commit 0afbba8
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 6 deletions.
17 changes: 16 additions & 1 deletion src/koa-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,22 @@ export const implementRoute = <
// 1. Validate the input data.
// 1a. For GET and DELETE, validate the query params.
if (['GET', 'DELETE'].includes(method)) {
ctx.request.query = parse(ctx, ctx.request.query);
// We want to allow data modifications during `parse`. But, Koa
// will not let us re-set the `.query` property entirely. So, we
// have to manually remove each key, then use Object.assign(...)
// to re-add the parsed data.

// This spread operator is important. Why:
// Some simple `parse` implementations will simply validate the
// ctx.request.query data, then return the same object as the "parsed"
// response. In that scenario, we need to make a copy of the data before
// deleting keys on ctx.request.query, because that would _also_ delete
// the keys on the object returned from parse(...)
const query = { ...parse(ctx, ctx.request.query) };
for (const key in ctx.request.query) {
delete ctx.request.query[key];
}
Object.assign(ctx.request.query, query);
} else {
// 1b. Otherwise, use the body.
ctx.request.body = parse(ctx, ctx.request.body);
Expand Down
83 changes: 80 additions & 3 deletions src/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { format } from 'prettier';
import Koa = require('koa');
import Router = require('@koa/router');
import bodyparser = require('koa-bodyparser');
import { OneSchemaRouter } from './router';
import { NamedClientFor, OneSchemaRouter } from './router';
import { z } from 'zod';
import { generateAxiosClient } from './generate-axios-client';

Expand All @@ -16,11 +16,18 @@ afterEach(() => {

const setup = <T extends OneSchemaRouter<any, any>>(
expose: (router: OneSchemaRouter<{}, Router>) => T,
): { client: AxiosInstance } => {
): { client: AxiosInstance; typed: NamedClientFor<T> } => {
const router = expose(
OneSchemaRouter.create({ using: new Router(), introspection: undefined }),
);
return serve(router);

const { client } = serve(router);

return {
client,
// @ts-expect-error TS is too dumb to know that this is right.
typed: router.client(client),
};
};

const serve = (
Expand Down Expand Up @@ -248,6 +255,76 @@ describe('type inference', () => {
}),
);
});

test('type inference when using Zod transforms', async () => {
const clients = setup((router) =>
router
.declare({
name: 'getItems',
route: 'GET /items',
request: z.object({
// API should enforce a string, but the code should receive a number.
message: z.string().transform((val) => Number(val)),
}),
response: z.object({
// The code should return a number, and the API should return a string.
message: z.number(),
}),
})
.implement('GET /items', (ctx) => {
// this statement helps us validate that the message is typed as a Number
ctx.request.query.message.toFixed();
// This should compile -- it's enforcing a number.
return { message: 1 };
})
.declare({
name: 'createItem',
route: 'POST /items',
request: z.object({
// API should enforce a string, but the code should receive a number.
message: z.string().transform((val) => Number(val)),
}),
response: z.object({
// The code should return a number, and the API should return a string.
message: z.number(),
}),
})
.implement('POST /items', (ctx) => {
// this statement helps us validate that the message is typed as a Number
ctx.request.body.message.toFixed();
// This should compile -- it's enforcing a number.
return { message: 2 };
}),
);
// ---- GET VALIDATIONS ----

// Assert that client types enforce the API types.
const getInput: Parameters<typeof clients.typed.getItems>[0] = {} as any;
// @ts-expect-error This should fail -- the API requires a string
getInput.message = 1;

// This should compile -- the API requires a string.
const getResult = await clients.typed.getItems({ message: '1' });

// Confirm response is runtime correct.
expect(getResult.data).toStrictEqual({ message: 1 });
// this statement helps us validate that the response is typed correctly, as a number.
getResult.data.message.toFixed();

// ---- POST VALIDATIONS ----
// Assert that client types enforce the API types.
const postInput: Parameters<typeof clients.typed.createItem>[0] = {} as any;
// @ts-expect-error This should fail -- the API requires a string
postInput.message = 1;

// This should compile -- the API requires a string.
const postResult = await clients.typed.createItem({ message: '1' });

// Confirm response is runtime correct.
expect(postResult.data).toStrictEqual({ message: 2 });
// this statement helps us validate that the response is typed correctly, as a number.
postResult.data.message.toFixed();
});
});

describe('input validation', () => {
Expand Down
11 changes: 9 additions & 2 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ export type OneSchemaRouterConfig<R extends Router<any, any>> = {

export type NamedClient<Schema extends ZodSchema> = {
[Route in keyof Schema as Schema[Route]['name']]: (
request: z.infer<Schema[Route]['request']> & PathParamsOf<Route>,
request: z.input<Schema[Route]['request']> & PathParamsOf<Route>,
config?: AxiosRequestConfig,
) => Promise<AxiosResponse<z.infer<Schema[Route]['response']>>>;
};

export type NamedClientFor<Router> = Router extends OneSchemaRouter<
infer Schema,
any
>
? NamedClient<Schema>
: never;

export class OneSchemaRouter<
Schema extends ZodSchema,
R extends Router<any, any>,
Expand Down Expand Up @@ -100,7 +107,7 @@ export class OneSchemaRouter<
route: Route,
implementation: EndpointImplementation<
Route,
z.infer<Schema[Route]['request']>,
z.output<Schema[Route]['request']>,
z.infer<Schema[Route]['response']>,
R
>,
Expand Down

0 comments on commit 0afbba8

Please sign in to comment.