diff --git a/README.md b/README.md index 5fb20d75..8a312148 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,9 @@ export const appRouter = trpc.router().query('sayHello', { }); // Client -const res = await fetch('http://localhost:3000/say-hello/James?greeting=Hello' /* 👈 */, { method: 'GET' }); +const res = await fetch('http://localhost:3000/say-hello/James?greeting=Hello' /* 👈 */, { + method: 'GET', +}); const body = await res.json(); /* { ok: true, data: { greeting: 'Hello James!' } } */ ``` @@ -243,7 +245,7 @@ export const appRouter = trpc.router().query('sayHello', { ```typescript const res = await fetch('http://localhost:3000/say-hello', { method: 'GET', - headers: { 'Authorization': 'Bearer usr_123' }, /* 👈 */ + headers: { Authorization: 'Bearer usr_123' } /* 👈 */, }); const body = await res.json(); /* { ok: true, data: { greeting: 'Hello James!' } } */ ``` @@ -311,7 +313,8 @@ Please see [full typings here](src/types.ts). | `protect` | `boolean` | Requires this endpoint to use an `Authorization` header credential with `Bearer` scheme on OpenAPI document. | `false` | `false` | | `summary` | `string` | A short summary of the endpoint included in the OpenAPI document. | `false` | `undefined` | | `description` | `string` | A verbose description of the endpoint included in the OpenAPI document. | `false` | `undefined` | -| `tag` | `string` | A tag used for logical grouping of endpoints in the OpenAPI document. | `false` | `undefined` | +| `tag` | `string` | A single tag used for logical grouping of endpoints in the OpenAPI document. | `false` | `undefined` | +| `tags` | `string[]` | A list of tags used for logical grouping of endpoints in the OpenAPI document. | `false` | `undefined` | #### CreateOpenApiNodeHttpHandlerOptions diff --git a/examples/with-express/src/router.ts b/examples/with-express/src/router.ts index 8255e0f0..cfe72d67 100644 --- a/examples/with-express/src/router.ts +++ b/examples/with-express/src/router.ts @@ -63,7 +63,7 @@ const authRouter = createRouter() enabled: true, method: 'POST', path: '/auth/register', - tag: 'auth', + tags: ['auth'], summary: 'Register as a new user', }, }, @@ -107,7 +107,7 @@ const authRouter = createRouter() enabled: true, method: 'POST', path: '/auth/login', - tag: 'auth', + tags: ['auth'], summary: 'Login as an existing user', }, }, @@ -147,7 +147,7 @@ const usersRouter = createRouter() enabled: true, method: 'GET', path: '/users', - tag: 'users', + tags: ['users'], summary: 'Read all users', }, }, @@ -177,7 +177,7 @@ const usersRouter = createRouter() enabled: true, method: 'GET', path: '/users/{id}', - tag: 'users', + tags: ['users'], summary: 'Read a user by id', }, }, @@ -212,7 +212,7 @@ const postsRouter = createRouter() enabled: true, method: 'GET', path: '/posts', - tag: 'posts', + tags: ['posts'], summary: 'Read all posts', }, }, @@ -246,7 +246,7 @@ const postsRouter = createRouter() enabled: true, method: 'GET', path: '/posts/{id}', - tag: 'posts', + tags: ['posts'], summary: 'Read a post by id', }, }, @@ -281,7 +281,7 @@ const postsProtectedRouter = createProtectedRouter() enabled: true, method: 'POST', path: '/posts', - tag: 'posts', + tags: ['posts'], protect: true, summary: 'Create a new post', }, @@ -314,7 +314,7 @@ const postsProtectedRouter = createProtectedRouter() enabled: true, method: 'PUT', path: '/posts/{id}', - tag: 'posts', + tags: ['posts'], protect: true, summary: 'Update an existing post', }, @@ -357,7 +357,7 @@ const postsProtectedRouter = createProtectedRouter() enabled: true, method: 'DELETE', path: '/posts/{id}', - tag: 'posts', + tags: ['posts'], protect: true, summary: 'Delete a post', }, diff --git a/examples/with-nextjs/src/server/router.ts b/examples/with-nextjs/src/server/router.ts index 7006a58e..364e2426 100644 --- a/examples/with-nextjs/src/server/router.ts +++ b/examples/with-nextjs/src/server/router.ts @@ -60,7 +60,7 @@ const authRouter = createRouter() enabled: true, method: 'POST', path: '/auth/register', - tag: 'auth', + tags: ['auth'], summary: 'Register as a new user', }, }, @@ -104,7 +104,7 @@ const authRouter = createRouter() enabled: true, method: 'POST', path: '/auth/login', - tag: 'auth', + tags: ['auth'], summary: 'Login as an existing user', }, }, @@ -144,7 +144,7 @@ const usersRouter = createRouter() enabled: true, method: 'GET', path: '/users', - tag: 'users', + tags: ['users'], summary: 'Read all users', }, }, @@ -174,7 +174,7 @@ const usersRouter = createRouter() enabled: true, method: 'GET', path: '/users/{id}', - tag: 'users', + tags: ['users'], summary: 'Read a user by id', }, }, @@ -209,7 +209,7 @@ const postsRouter = createRouter() enabled: true, method: 'GET', path: '/posts', - tag: 'posts', + tags: ['posts'], summary: 'Read all posts', }, }, @@ -243,7 +243,7 @@ const postsRouter = createRouter() enabled: true, method: 'GET', path: '/posts/{id}', - tag: 'posts', + tags: ['posts'], summary: 'Read a post by id', }, }, @@ -278,7 +278,7 @@ const postsProtectedRouter = createProtectedRouter() enabled: true, method: 'POST', path: '/posts', - tag: 'posts', + tags: ['posts'], protect: true, summary: 'Create a new post', }, @@ -311,7 +311,7 @@ const postsProtectedRouter = createProtectedRouter() enabled: true, method: 'PUT', path: '/posts/{id}', - tag: 'posts', + tags: ['posts'], protect: true, summary: 'Update an existing post', }, @@ -354,7 +354,7 @@ const postsProtectedRouter = createProtectedRouter() enabled: true, method: 'DELETE', path: '/posts/{id}', - tag: 'posts', + tags: ['posts'], protect: true, summary: 'Delete a post', }, diff --git a/src/generator/paths.ts b/src/generator/paths.ts index c1c7bd38..52dce093 100644 --- a/src/generator/paths.ts +++ b/src/generator/paths.ts @@ -14,7 +14,7 @@ export const getOpenApiPathsObject = ( forEachOpenApiProcedure(queries, ({ path: queryPath, procedure, openapi }) => { try { - const { method, protect, summary, description, tag } = openapi; + const { method, protect, summary, description, tags, tag } = openapi; if (method !== 'GET' && method !== 'DELETE') { throw new TRPCError({ message: 'Query method must be GET or DELETE', @@ -40,7 +40,7 @@ export const getOpenApiPathsObject = ( operationId: queryPath, summary, description, - tags: tag ? [tag] : undefined, + tags: tags ?? (tag ? [tag] : undefined), security: protect ? [{ Authorization: [] }] : undefined, parameters: getParameterObjects(inputParser, pathParameters, 'all'), responses: getResponsesObject(outputParser), @@ -55,7 +55,7 @@ export const getOpenApiPathsObject = ( forEachOpenApiProcedure(mutations, ({ path: mutationPath, procedure, openapi }) => { try { - const { method, protect, summary, description, tag } = openapi; + const { method, protect, summary, description, tags, tag } = openapi; if (method !== 'POST' && method !== 'PATCH' && method !== 'PUT') { throw new TRPCError({ message: 'Mutation method must be POST, PATCH or PUT', @@ -81,7 +81,7 @@ export const getOpenApiPathsObject = ( operationId: mutationPath, summary, description, - tags: tag ? [tag] : undefined, + tags: tags ?? (tag ? [tag] : undefined), security: protect ? [{ Authorization: [] }] : undefined, requestBody: getRequestBodyObject(inputParser, pathParameters), parameters: getParameterObjects(inputParser, pathParameters, 'path'), diff --git a/src/types.ts b/src/types.ts index b9f7525e..2a52d1b4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,8 +15,7 @@ export type OpenApiMeta> = TMeta & { summary?: string; description?: string; protect?: boolean; - tag?: string; - }; + } & ({ tag?: never; tags?: string[] } | { tag?: string; tags?: never }); }; export type OpenApiProcedureRecord> = ProcedureRecord< diff --git a/test/generator.test.ts b/test/generator.test.ts index 82d7e382..14cd5086 100644 --- a/test/generator.test.ts +++ b/test/generator.test.ts @@ -888,7 +888,7 @@ describe('generator', () => { expect(Object.keys(openApiDocument.paths).length).toBe(0); }); - test('with summary, description & tag', () => { + test('with summary, description & single tag', () => { const appRouter = trpc.router().query('all.metadata', { meta: { openapi: { @@ -917,6 +917,35 @@ describe('generator', () => { expect(openApiDocument.paths['/metadata/all']!.get!.tags).toEqual(['tag']); }); + test('with summary, description & multiple tags', () => { + const appRouter = trpc.router().query('all.metadata', { + meta: { + openapi: { + enabled: true, + path: '/metadata/all', + method: 'GET', + summary: 'Short summary', + description: 'Verbose description', + tags: ['tagA', 'tagB'], + }, + }, + input: z.object({ name: z.string() }), + output: z.object({ name: z.string() }), + resolve: ({ input }) => ({ name: input.name }), + }); + + 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['/metadata/all']!.get!.summary).toBe('Short summary'); + expect(openApiDocument.paths['/metadata/all']!.get!.description).toBe('Verbose description'); + expect(openApiDocument.paths['/metadata/all']!.get!.tags).toEqual(['tagA', 'tagB']); + }); + test('with security', () => { const appRouter = trpc.router().mutation('protectedEndpoint', { meta: {