Skip to content

Commit

Permalink
feat/custom-headers (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
jlalmes authored Aug 22, 2022
1 parent efd94e5 commit f75a78f
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 16 deletions.
16 changes: 12 additions & 4 deletions src/generator/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const getOpenApiPathsObject = (

forEachOpenApiProcedure(queries, ({ path: queryPath, procedure, openapi }) => {
try {
const { method, protect, summary, description, tags, tag } = openapi;
const { method, protect, summary, description, tags, tag, headers } = openapi;
if (method !== 'GET' && method !== 'DELETE') {
throw new TRPCError({
message: 'Query method must be GET or DELETE',
Expand All @@ -24,6 +24,7 @@ export const getOpenApiPathsObject = (

const path = normalizePath(openapi.path);
const pathParameters = getPathParameters(path);
const headerParameters = headers?.map((header) => ({ ...header, in: 'header' })) || [];
const httpMethod = OpenAPIV3.HttpMethods[method];
if (pathsObject[path]?.[httpMethod]) {
throw new TRPCError({
Expand All @@ -42,7 +43,10 @@ export const getOpenApiPathsObject = (
description,
tags: tags ?? (tag ? [tag] : undefined),
security: protect ? [{ Authorization: [] }] : undefined,
parameters: getParameterObjects(inputParser, pathParameters, 'all'),
parameters: [
...headerParameters,
...(getParameterObjects(inputParser, pathParameters, 'all') || []),
],
responses: getResponsesObject(outputParser),
},
};
Expand All @@ -55,7 +59,7 @@ export const getOpenApiPathsObject = (

forEachOpenApiProcedure(mutations, ({ path: mutationPath, procedure, openapi }) => {
try {
const { method, protect, summary, description, tags, tag } = openapi;
const { method, protect, summary, description, tags, tag, headers } = openapi;
if (method !== 'POST' && method !== 'PATCH' && method !== 'PUT') {
throw new TRPCError({
message: 'Mutation method must be POST, PATCH or PUT',
Expand All @@ -65,6 +69,7 @@ export const getOpenApiPathsObject = (

const path = normalizePath(openapi.path);
const pathParameters = getPathParameters(path);
const headerParameters = headers?.map((header) => ({ ...header, in: 'header' })) || [];
const httpMethod = OpenAPIV3.HttpMethods[method];
if (pathsObject[path]?.[httpMethod]) {
throw new TRPCError({
Expand All @@ -84,7 +89,10 @@ export const getOpenApiPathsObject = (
tags: tags ?? (tag ? [tag] : undefined),
security: protect ? [{ Authorization: [] }] : undefined,
requestBody: getRequestBodyObject(inputParser, pathParameters),
parameters: getParameterObjects(inputParser, pathParameters, 'path'),
parameters: [
...headerParameters,
...(getParameterObjects(inputParser, pathParameters, 'path') || []),
],
responses: getResponsesObject(outputParser),
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProcedureRecord } from '@trpc/server';
import { DefaultErrorShape, Router } from '@trpc/server/dist/declarations/src/router';
// eslint-disable-next-line import/no-unresolved
import { TRPC_ERROR_CODE_KEY } from '@trpc/server/dist/declarations/src/rpc';
import { OpenAPIV3 } from 'openapi-types';
import { ZodIssue, z } from 'zod';

export type OpenApiMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
Expand All @@ -15,6 +16,7 @@ export type OpenApiMeta<TMeta = Record<string, any>> = TMeta & {
summary?: string;
description?: string;
protect?: boolean;
headers?: (OpenAPIV3.ParameterBaseObject & { name: string; in?: 'header' })[];
} & (
| {
tags?: never;
Expand Down
70 changes: 58 additions & 12 deletions test/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ describe('generator', () => {
"get": Object {
"description": undefined,
"operationId": "readUsers",
"parameters": undefined,
"parameters": Array [],
"responses": Object {
"200": Object {
"content": Object {
Expand Down Expand Up @@ -888,7 +888,7 @@ describe('generator', () => {
expect(Object.keys(openApiDocument.paths).length).toBe(0);
});

test('with summary, description & single tag', () => {
test('with summary, description & multiple tags', () => {
const appRouter = trpc.router<any, OpenApiMeta>().query('all.metadata', {
meta: {
openapi: {
Expand All @@ -897,7 +897,7 @@ describe('generator', () => {
method: 'GET',
summary: 'Short summary',
description: 'Verbose description',
tag: 'tag',
tags: ['tagA', 'tagB'],
},
},
input: z.object({ name: z.string() }),
Expand All @@ -914,19 +914,18 @@ describe('generator', () => {
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(['tag']);
expect(openApiDocument.paths['/metadata/all']!.get!.tags).toEqual(['tagA', 'tagB']);
});

test('with summary, description & multiple tags', () => {
// @deprecated
test('with single tag', () => {
const appRouter = trpc.router<any, OpenApiMeta>().query('all.metadata', {
meta: {
openapi: {
enabled: true,
path: '/metadata/all',
method: 'GET',
summary: 'Short summary',
description: 'Verbose description',
tags: ['tagA', 'tagB'],
tag: 'tag',
},
},
input: z.object({ name: z.string() }),
Expand All @@ -941,9 +940,7 @@ describe('generator', () => {
});

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']);
expect(openApiDocument.paths['/metadata/all']!.get!.tags).toEqual(['tag']);
});

test('with security', () => {
Expand Down Expand Up @@ -1185,7 +1182,7 @@ describe('generator', () => {
});

expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
expect(openApiDocument.paths['/void']!.get!.parameters).toMatchInlineSnapshot(`undefined`);
expect(openApiDocument.paths['/void']!.get!.parameters).toEqual([]);
expect(openApiDocument.paths['/void']!.get!.responses[200]).toMatchInlineSnapshot(`
Object {
"content": Object {
Expand Down Expand Up @@ -2125,4 +2122,53 @@ describe('generator', () => {
}
`);
});

test('with custom header', () => {
const appRouter = trpc.router<any, OpenApiMeta>().query('echo', {
meta: {
openapi: {
enabled: true,
path: '/echo',
method: 'GET',
headers: [
{
name: 'x-custom-header',
required: true,
description: 'Some custom header',
},
],
},
},
input: z.object({ id: z.string() }),
output: z.object({ id: z.string() }),
resolve: ({ input }) => ({ id: input.id }),
});

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['/echo']!.get!.parameters).toMatchInlineSnapshot(`
Array [
Object {
"description": "Some custom header",
"in": "header",
"name": "x-custom-header",
"required": true,
},
Object {
"description": undefined,
"in": "query",
"name": "id",
"required": true,
"schema": Object {
"type": "string",
},
},
]
`);
});
});

0 comments on commit f75a78f

Please sign in to comment.