From 35e9ed882fd82eee9f85b1a5d41ab4b5e6c568d5 Mon Sep 17 00:00:00 2001 From: Patrick McLaughlin Date: Tue, 26 Apr 2022 10:21:54 -0400 Subject: [PATCH] fix: use consistent format for path parameters between server and client Ticket: BG-46917 --- packages/express-wrapper/src/index.ts | 5 +- packages/express-wrapper/src/path.ts | 8 ++++ packages/express-wrapper/test/path.test.ts | 21 +++++++++ .../test/{test-server.ts => server.test.ts} | 46 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/express-wrapper/src/path.ts create mode 100644 packages/express-wrapper/test/path.test.ts rename packages/express-wrapper/test/{test-server.ts => server.test.ts} (83%) diff --git a/packages/express-wrapper/src/index.ts b/packages/express-wrapper/src/index.ts index 274d28fe..4fe1cf13 100644 --- a/packages/express-wrapper/src/index.ts +++ b/packages/express-wrapper/src/index.ts @@ -14,6 +14,8 @@ import { ResponseType, } from '@api-ts/io-ts-http'; +import { apiTsPathToExpress } from './path'; + export type Function = ( input: RequestType, ) => ResponseType | Promise>; @@ -141,7 +143,8 @@ export function createServer( ); const handlers = [...stack.slice(0, stack.length - 1), handler]; - router[method](httpRoute.path, handlers); + const expressPath = apiTsPathToExpress(httpRoute.path); + router[method](expressPath, handlers); } } diff --git a/packages/express-wrapper/src/path.ts b/packages/express-wrapper/src/path.ts new file mode 100644 index 00000000..21752d1f --- /dev/null +++ b/packages/express-wrapper/src/path.ts @@ -0,0 +1,8 @@ +// Converts an io-ts-http path to an express one +// assumes that only simple path parameters are present and the wildcard features in express +// arent used. + +const PATH_PARAM = /{(\w+)}/g; + +export const apiTsPathToExpress = (inputPath: string) => + inputPath.replace(PATH_PARAM, ':$1'); diff --git a/packages/express-wrapper/test/path.test.ts b/packages/express-wrapper/test/path.test.ts new file mode 100644 index 00000000..4be371f1 --- /dev/null +++ b/packages/express-wrapper/test/path.test.ts @@ -0,0 +1,21 @@ +import test from 'ava'; + +import { apiTsPathToExpress } from '../src/path'; + +test('should pass through paths with no parameters', (t) => { + const input = '/foo/bar'; + const output = apiTsPathToExpress(input); + t.deepEqual(output, input); +}); + +test('should translate a path segment that specifies a parameter', (t) => { + const input = '/foo/{bar}'; + const output = apiTsPathToExpress(input); + t.deepEqual(output, '/foo/:bar'); +}); + +test('should translate multiple path segments', (t) => { + const input = '/foo/{bar}/baz/{id}'; + const output = apiTsPathToExpress(input); + t.deepEqual(output, '/foo/:bar/baz/:id'); +}); diff --git a/packages/express-wrapper/test/test-server.ts b/packages/express-wrapper/test/server.test.ts similarity index 83% rename from packages/express-wrapper/test/test-server.ts rename to packages/express-wrapper/test/server.test.ts index d1956f4a..4ac08d43 100644 --- a/packages/express-wrapper/test/test-server.ts +++ b/packages/express-wrapper/test/server.test.ts @@ -39,9 +39,25 @@ const PutHello = httpRoute({ }); type PutHello = typeof PutHello; +const GetHello = httpRoute({ + path: '/hello/{id}', + method: 'GET', + request: httpRequest({ + params: { + id: t.string, + }, + }), + response: { + ok: t.type({ + id: t.string, + }), + }, +}); + const ApiSpec = apiSpec({ 'hello.world': { put: PutHello, + get: GetHello, }, }); @@ -76,6 +92,8 @@ const CreateHelloWorld = async (parameters: { }); }; +const GetHelloWorld = async (params: { id: string }) => Response.ok(params); + test('should offer a delightful developer experience', async (t) => { const app = createServer(ApiSpec, (app: express.Application) => { // Configure app-level middleware @@ -84,6 +102,7 @@ test('should offer a delightful developer experience', async (t) => { return { 'hello.world': { put: [routeMiddleware, CreateHelloWorld], + get: [GetHelloWorld], }, }; }); @@ -105,6 +124,29 @@ test('should offer a delightful developer experience', async (t) => { t.like(response, { message: "Who's there?" }); }); +test('should handle io-ts-http formatted path parameters', async (t) => { + const app = createServer(ApiSpec, (app: express.Application) => { + app.use(express.json()); + app.use(appMiddleware); + return { + 'hello.world': { + put: [routeMiddleware, CreateHelloWorld], + get: [GetHelloWorld], + }, + }; + }); + + const server = supertest(app); + const apiClient = buildApiClient(supertestRequestFactory(server), ApiSpec); + + const response = await apiClient['hello.world'] + .get({ id: '1337' }) + .decodeExpecting(200) + .then((res) => res.body); + + t.like(response, { id: '1337' }); +}); + test('should invoke app-level middleware', async (t) => { const app = createServer(ApiSpec, (app: express.Application) => { // Configure app-level middleware @@ -113,6 +155,7 @@ test('should invoke app-level middleware', async (t) => { return { 'hello.world': { put: [CreateHelloWorld], + get: [GetHelloWorld], }, }; }); @@ -135,6 +178,7 @@ test('should invoke route-level middleware', async (t) => { return { 'hello.world': { put: [routeMiddleware, CreateHelloWorld], + get: [GetHelloWorld], }, }; }); @@ -157,6 +201,7 @@ test('should infer status code from response type', async (t) => { return { 'hello.world': { put: [CreateHelloWorld], + get: [GetHelloWorld], }, }; }); @@ -179,6 +224,7 @@ test('should return a 400 when request fails to decode', async (t) => { return { 'hello.world': { put: [CreateHelloWorld], + get: [GetHelloWorld], }, }; });