From ecacdf92c5a992ab74b3a3dc577d7f3234174a92 Mon Sep 17 00:00:00 2001 From: James Berry Date: Tue, 5 Jul 2022 11:02:58 +0100 Subject: [PATCH 1/3] fix/optional-query-input-schema --- src/generator/schema.ts | 10 ++++++++-- src/utils.ts | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/generator/schema.ts b/src/generator/schema.ts index 721def46..7ef7c407 100644 --- a/src/generator/schema.ts +++ b/src/generator/schema.ts @@ -6,6 +6,7 @@ import zodToJsonSchema from 'zod-to-json-schema'; import { instanceofZod, instanceofZodTypeLikeObject, + instanceofZodTypeLikeOptional, instanceofZodTypeLikeString, instanceofZodTypeLikeVoid, } from '../utils'; @@ -60,7 +61,8 @@ export const getParameterObjects = ( return true; }) .map((shapeKey) => { - const shapeSchema = shape[shapeKey]!; + let shapeSchema = shape[shapeKey]!; + const isRequired = !shapeSchema.isOptional(); if (!instanceofZodTypeLikeString(shapeSchema)) { throw new TRPCError({ @@ -69,13 +71,17 @@ export const getParameterObjects = ( }); } + if (instanceofZodTypeLikeOptional(shapeSchema)) { + shapeSchema = shapeSchema.unwrap(); + } + const isPathParameter = pathParameters.includes(shapeKey); const { description, ...schema } = zodSchemaToOpenApiSchemaObject(shapeSchema); return { name: shapeKey, in: isPathParameter ? 'path' : 'query', - required: isPathParameter || !shapeSchema.isOptional(), + required: isPathParameter || isRequired, schema: schema, description: description, }; diff --git a/src/utils.ts b/src/utils.ts index 796b8df6..a0596a72 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -72,3 +72,9 @@ export const instanceofZodTypeLikeString = (type: z.ZodTypeAny): boolean => { } return instanceofZodTypeKind(type, z.ZodFirstPartyTypeKind.ZodString); }; + +export const instanceofZodTypeLikeOptional = ( + type: z.ZodTypeAny, +): type is z.ZodOptional => { + return instanceofZodTypeKind(type, z.ZodFirstPartyTypeKind.ZodOptional); +}; From 9bbbc041a3b089888826aa6d3a14f103f58f3a67 Mon Sep 17 00:00:00 2001 From: James Berry Date: Tue, 5 Jul 2022 11:41:01 +0100 Subject: [PATCH 2/3] throw error on optional path params --- src/generator/schema.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/generator/schema.ts b/src/generator/schema.ts index 7ef7c407..f167006e 100644 --- a/src/generator/schema.ts +++ b/src/generator/schema.ts @@ -63,6 +63,7 @@ export const getParameterObjects = ( .map((shapeKey) => { let shapeSchema = shape[shapeKey]!; const isRequired = !shapeSchema.isOptional(); + const isPathParameter = pathParameters.includes(shapeKey); if (!instanceofZodTypeLikeString(shapeSchema)) { throw new TRPCError({ @@ -72,10 +73,15 @@ export const getParameterObjects = ( } if (instanceofZodTypeLikeOptional(shapeSchema)) { + if (isPathParameter) { + throw new TRPCError({ + message: `Path parameter: "${shapeKey}" must not be optional`, + code: 'INTERNAL_SERVER_ERROR', + }); + } shapeSchema = shapeSchema.unwrap(); } - const isPathParameter = pathParameters.includes(shapeKey); const { description, ...schema } = zodSchemaToOpenApiSchemaObject(shapeSchema); return { From 93879592389e8a89557519f3d825cad7d9c0fbf9 Mon Sep 17 00:00:00 2001 From: James Berry Date: Tue, 5 Jul 2022 11:41:23 +0100 Subject: [PATCH 3/3] add tests --- test/generator.test.ts | 75 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/test/generator.test.ts b/test/generator.test.ts index 2389aa89..ccc7986e 100644 --- a/test/generator.test.ts +++ b/test/generator.test.ts @@ -1,6 +1,5 @@ import * as trpc from '@trpc/server'; import { Subscription } from '@trpc/server'; -import e from 'express'; import openAPISchemaValidator from 'openapi-schema-validator'; import { z } from 'zod'; @@ -620,6 +619,23 @@ describe('generator', () => { }).toThrowError('[query.pathParameters] - Input parser must be a ZodObject'); }); + test('with optional path parameters', () => { + const appRouter = trpc.router().query('pathParameters', { + meta: { openapi: { enabled: true, path: '/path-parameters/{name}', method: 'GET' } }, + input: z.object({ name: z.string().optional() }), + output: z.object({ name: z.string() }), + resolve: () => ({ name: 'asdf' }), + }); + + expect(() => { + generateOpenApiDocument(appRouter, { + title: 'tRPC OpenAPI', + version: '1.0.0', + baseUrl: 'http://localhost:3000/api', + }); + }).toThrowError('[query.pathParameters] - Path parameter: "name" must not be optional'); + }); + test('with missing path parameters', () => { const appRouter = trpc.router().query('pathParameters', { meta: { openapi: { enabled: true, path: '/path-parameters/{name}', method: 'GET' } }, @@ -1577,4 +1593,61 @@ describe('generator', () => { } `); }); + + test('with optional query input parameter', () => { + const appRouter = trpc.router().query('optional', { + meta: { openapi: { enabled: true, path: '/optional', method: 'GET' } }, + input: z.object({ payload: z.string().optional() }), + output: z.string().optional(), + resolve: ({ input }) => input.payload, + }); + + const openApiDocument = generateOpenApiDocument(appRouter, { + title: 'tRPC OpenAPI', + version: '1.0.0', + baseUrl: 'http://localhost:3000/api', + }); + + expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]); + expect(openApiDocument.paths['/optional']!.get!.parameters).toMatchInlineSnapshot(` + Array [ + Object { + "description": undefined, + "in": "query", + "name": "payload", + "required": false, + "schema": Object { + "type": "string", + }, + }, + ] + `); + expect(openApiDocument.paths['/optional']!.get!.responses[200]).toMatchInlineSnapshot(` + Object { + "content": Object { + "application/json": Object { + "schema": Object { + "additionalProperties": false, + "properties": Object { + "data": Object { + "type": "string", + }, + "ok": Object { + "enum": Array [ + true, + ], + "type": "boolean", + }, + }, + "required": Array [ + "ok", + ], + "type": "object", + }, + }, + }, + "description": "Successful response", + } + `); + }); });