From dc825b3b577260a23fc6bc7def81333e9cb3f64e Mon Sep 17 00:00:00 2001 From: Swain Molster Date: Wed, 7 Jun 2023 22:00:00 -0400 Subject: [PATCH 1/2] feat!: enforce response conforms to schema --- src/router.test.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++-- src/router.ts | 12 ++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/router.test.ts b/src/router.test.ts index 1fd9355..6babb49 100644 --- a/src/router.test.ts +++ b/src/router.test.ts @@ -328,6 +328,58 @@ describe('input validation', () => { }); }); +describe('output validation', () => { + (['GET', 'DELETE', 'POST', 'PUT', 'PATCH'] as const).forEach((method) => { + test(`${method} requests throw an Error when response type does not match the schema`, async () => { + const { client } = setup((router) => + router + .declare({ + name: 'createItem', + route: `${method} /items`, + request: z.object({}), + response: z.object({ message: z.string() }), + }) + .implement(`${method} /items`, () => ({ + // @ts-expect-error Intentionally writing incorrect TS here + message: 123, + })), + ); + + const { status } = await client.request({ + method, + url: '/items', + }); + + expect(status).toStrictEqual(500); + }); + + test(`${method} requests allow extra fields on response values and strip them from responses`, async () => { + const { client } = setup((router) => + router + .declare({ + name: 'createItem', + route: `${method} /items`, + request: z.object({}), + response: z.object({ message: z.string() }), + }) + .implement(`${method} /items`, () => ({ + message: 'test-message', + anotherField: 1234, + })), + ); + + const { status, data } = await client.request({ + method, + url: '/items', + }); + + expect(status).toStrictEqual(200); + // Ensure `anotherField` was stripped. + expect(data).toStrictEqual({ message: 'test-message' }); + }); + }); +}); + describe('implementations', () => { (['POST', 'PUT', 'PATCH'] as const).forEach((method) => { test(`rejects requests that do not match the schema for ${method} requests`, async () => { @@ -471,8 +523,6 @@ describe('introspection', () => { const introspectionResult = await client.get('/private/introspection'); - console.log(JSON.stringify(introspectionResult.data, null, 2)); - expect(introspectionResult.data.schema).toStrictEqual({ Endpoints: { 'POST /items': { diff --git a/src/router.ts b/src/router.ts index 7170485..0f7d701 100644 --- a/src/router.ts +++ b/src/router.ts @@ -120,7 +120,17 @@ export class OneSchemaRouter< } return res.data; }, - implementation, + async (ctx) => { + const result = await implementation(ctx); + const res = endpoint.response.safeParse(result); + if (!res.success) { + return ctx.throw( + 500, + `A response value from endpoint '${route}' did not conform to the response schema.`, + ); + } + return res.data; + }, ); return this; From 8bfc07e9f4bf0e53e3e63f4ef2820ad91ef7bbbd Mon Sep 17 00:00:00 2001 From: Swain Molster Date: Thu, 8 Jun 2023 10:49:56 -0400 Subject: [PATCH 2/2] add validation error message --- src/router.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/router.ts b/src/router.ts index 0f7d701..4dd348f 100644 --- a/src/router.ts +++ b/src/router.ts @@ -124,10 +124,10 @@ export class OneSchemaRouter< const result = await implementation(ctx); const res = endpoint.response.safeParse(result); if (!res.success) { - return ctx.throw( - 500, - `A response value from endpoint '${route}' did not conform to the response schema.`, - ); + const friendlyError = fromZodError(res.error, { + prefix: `A response value from endpoint '${route}' did not conform to the response schema.`, + }); + return ctx.throw(500, friendlyError.message); } return res.data; },