From ff2dc069f79cbfd6e586670b532f5d69f0363ba3 Mon Sep 17 00:00:00 2001 From: Swain Molster Date: Tue, 31 Oct 2023 19:54:18 -0400 Subject: [PATCH] feat: try out a new route declaration syntax, aimed at compatibility --- src/compat-router.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/router.test.ts | 55 ++++++++++++++++++++++++--- src/router.ts | 2 +- 4 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 src/compat-router.ts diff --git a/src/compat-router.ts b/src/compat-router.ts new file mode 100644 index 0000000..c333243 --- /dev/null +++ b/src/compat-router.ts @@ -0,0 +1,89 @@ +import Router from '@koa/router'; +import { z } from 'zod'; +import { + ContextOfEndpoint, + EndpointImplementation, + OneSchemaRouterMiddleware, +} from './koa-utils'; +import { + NamedClient, + OneSchemaRouter, + OneSchemaRouterConfig, + RouterEndpointDefinition, + ZodSchema, +} from './router'; +import { AxiosInstance } from 'axios'; + +type MethodMap = { + get: 'GET'; + post: 'POST'; + put: 'PUT'; + patch: 'PATCH'; + delete: 'DELETE'; +}; + +export type OneSchemaCompatRouter< + Schema extends ZodSchema, + R extends Router, +> = { + [Method in keyof MethodMap]: < + Path extends string, + Name extends string, + Endpoint extends RouterEndpointDefinition, + >( + path: Path, + meta: Endpoint, + ...middlewares: [ + ...OneSchemaRouterMiddleware< + ContextOfEndpoint< + `${MethodMap[Method]} ${Path}`, + z.output, + R + > + >[], + EndpointImplementation< + `${MethodMap[Method]} ${Path}`, + z.output, + z.infer, + R + >, + ] + ) => OneSchemaCompatRouter< + Schema & { + [Route in `${MethodMap[Method]} ${Path}`]: Endpoint; + }, + R + >; +} & { + client: (instance: AxiosInstance) => NamedClient; + middleware: () => Router.Middleware; +}; + +export const createCompatRouter = >( + config: OneSchemaRouterConfig, +): OneSchemaCompatRouter<{}, R> => { + const _router = OneSchemaRouter.create(config); + + const compatRouter: any = { + middleware: () => _router.middleware(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + client: (instance: any) => _router.client(instance), + }; + for (const method of ['get', 'post', 'put', 'patch', 'delete'] as const) { + compatRouter[method] = ( + path: string, + endpoint: any, + ...middlewares: any[] + ) => { + const route = `${method.toUpperCase()} ${path}`; + _router + .declare({ ...endpoint, route }) + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + .implement(route, ...middlewares); + + return compatRouter; + }; + } + return compatRouter; +}; diff --git a/src/index.ts b/src/index.ts index 6d62268..22c8845 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './koa'; export * from './types'; export * from './router'; +export * from './compat-router'; // We intentionally avoid exposing codegen-related modules here, // so that consumers don't unnecessarily bundle codegen logic diff --git a/src/router.test.ts b/src/router.test.ts index 5261bf5..51576ae 100644 --- a/src/router.test.ts +++ b/src/router.test.ts @@ -5,18 +5,21 @@ import { format } from 'prettier'; import Koa = require('koa'); import Router = require('@koa/router'); import bodyparser = require('koa-bodyparser'); -import { NamedClientFor, OneSchemaRouter } from './router'; +import { NamedClient, OneSchemaRouter, ZodSchema } from './router'; import { z } from 'zod'; import { generateAxiosClient } from './generate-axios-client'; +import { OneSchemaCompatRouter, createCompatRouter } from './compat-router'; let server: Server | undefined = undefined; afterEach(() => { server?.close(); }); -const setup = >( - expose: (router: OneSchemaRouter<{}, Router>) => T, -): { client: AxiosInstance; typed: NamedClientFor } => { +const setup = ( + expose: ( + router: OneSchemaRouter<{}, Router>, + ) => OneSchemaRouter | OneSchemaCompatRouter, +): { client: AxiosInstance; typed: NamedClient } => { const router = expose( OneSchemaRouter.create({ using: new Router(), introspection: undefined }), ); @@ -25,13 +28,12 @@ const setup = >( return { client, - // @ts-expect-error TS is too dumb to know that this is right. typed: router.client(client), }; }; const serve = ( - router: OneSchemaRouter<{}, Router>, + router: OneSchemaRouter | OneSchemaCompatRouter, ): { client: AxiosInstance } => { server = new Koa().use(bodyparser()).use(router.middleware()).listen(); @@ -942,3 +944,44 @@ test('the client(...) helper', async () => { expect(postResponse.status).toStrictEqual(200); expect(postResponse.data).toStrictEqual({ id: 'some-id' }); }); + +describe('compat router', () => { + test('get + post', async () => { + const { typed: client } = setup(() => + createCompatRouter({ introspection: undefined, using: new Router() }) + .post( + '/items', + { + name: 'createItem', + request: z.object({ id: z.string() }), + response: z.object({ id: z.string() }), + }, + (ctx) => ({ id: ctx.request.body.id }), + ) + .get( + '/items/:id', + { + name: 'getItemById', + request: z.object({ + filter: z.string(), + }), + response: z.object({ id: z.string() }), + }, + (ctx) => ({ + id: ctx.params.id + ':' + ctx.request.query.filter, + }), + ), + ); + + const res1 = await client.createItem({ id: 'test-id' }); + expect(res1.status).toStrictEqual(200); + expect(res1.data).toStrictEqual({ id: 'test-id' }); + + const res2 = await client.getItemById({ + id: 'test-id', + filter: 'test-filter', + }); + expect(res2.status).toStrictEqual(200); + expect(res2.data).toStrictEqual({ id: 'test-id:test-filter' }); + }); +}); diff --git a/src/router.ts b/src/router.ts index a06af6f..71162c4 100644 --- a/src/router.ts +++ b/src/router.ts @@ -24,7 +24,7 @@ export type RouterEndpointDefinition = { type Method = 'GET' | 'DELETE' | 'PUT' | 'POST' | 'PATCH'; type RoughRoute = `${Method} ${string}`; -type ZodSchema = { +export type ZodSchema = { [route: string]: RouterEndpointDefinition; };